diff --git a/.github/workflows/powershell.yml b/.github/workflows/powershell.yml index 60bdec1..419192b 100644 --- a/.github/workflows/powershell.yml +++ b/.github/workflows/powershell.yml @@ -41,6 +41,6 @@ jobs: # Upload the SARIF file generated in the previous step - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: results.sarif diff --git a/Test/Test.psm1 b/Test/Test.psm1 index 159f718..9c5d87f 100644 --- a/Test/Test.psm1 +++ b/Test/Test.psm1 @@ -1,4 +1,4 @@ -Write-Information -Message ("Loading {0} ..." -f ($PSCommandPath | Split-Path -LeafBase)) -InformationAction continue +Write-Information -MessageData ("Loading {0} ..." -f ($PSCommandPath | Split-Path -LeafBase)) -InformationAction continue #Module path is where resides the RootModule file. This file. :) $MODULE_PATH = $PSScriptRoot diff --git a/Test/helper/module.helper.ps1 b/Test/helper/module.helper.ps1 index 8eea760..1723a65 100644 --- a/Test/helper/module.helper.ps1 +++ b/Test/helper/module.helper.ps1 @@ -54,7 +54,7 @@ function Get-Ps1FullPath{ ) # If folderName is not empty - if($FolderName -ne $null){ + if($null -ne $FolderName){ $folder = Get-ModuleFolder -FolderName $FolderName -ModuleRootPath $ModuleRootPath $path = $folder | Join-Path -ChildPath $Name } else { @@ -102,6 +102,27 @@ function Get-ModuleName{ return $MODULE_NAME } +<# +.SYNOPSIS +Gets the full path of a specific module folder. + +.DESCRIPTION +Gets the full path of a specific module folder based on the folder name and optionally the module root path. + +.PARAMETER FolderName +The name of the folder to get the path for. Must be one of the valid folder names. + +.PARAMETER ModuleRootPath +The root path of the module. If not provided, uses the default module root path. + +.EXAMPLE +Get-ModuleFolder -FolderName "Public" +Gets the path to the Public folder of the current module. + +.EXAMPLE +Get-ModuleFolder -FolderName "Private" -ModuleRootPath "C:\MyModule" +Gets the path to the Private folder of the specified module. +#> function Get-ModuleFolder{ [CmdletBinding()] param( diff --git a/Test/include/invokeCommand.mock.ps1 b/Test/include/invokeCommand.mock.ps1 index 2c3370c..3836ce6 100644 --- a/Test/include/invokeCommand.mock.ps1 +++ b/Test/include/invokeCommand.mock.ps1 @@ -57,6 +57,19 @@ function MockCall{ Set-InvokeCommandMock -Alias $command -Command "Get-MockFileContent -filename $filename" } +function MockCallAsync{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][string] $filename + ) + + Assert-MockFileNotfound $fileName + + $moduleTest = $PSScriptRoot | Split-Path -Parent | Convert-Path + + Set-InvokeCommandMock -Alias $command -Command "Import-Module $moduleTest ; Get-MockFileContent -filename $filename" +} + function MockCallJson{ param( [Parameter(Position=0)][string] $command, @@ -69,6 +82,20 @@ function MockCallJson{ Set-InvokeCommandMock -Alias $command -Command "Get-MockFileContentJson -filename $filename" } +function MockCallJsonAsync{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][string] $filename + + ) + + Assert-MockFileNotfound $fileName + + $moduleTest = $PSScriptRoot | Split-Path -Parent | Convert-Path + + Set-InvokeCommandMock -Alias $command -Command "Import-Module $moduleTest ; Get-MockFileContentJson -filename $filename" +} + function Get-MockFileFullPath{ param( [parameter(Mandatory,Position=0)][string] $fileName @@ -130,7 +157,6 @@ function MockCallToObject{ Set-Variable -Name $varName -Value $OutObject -Scope Global Set-InvokeCommandMock -Alias $command -Command "(Get-Variable -Name $varName -Scope Global).Value" - } function MockCallToNull{ diff --git a/Test/private/mocks/clearProjectV2ItemFieldValue.json b/Test/private/mocks/clearProjectV2ItemFieldValue.json new file mode 100644 index 0000000..dd17bba --- /dev/null +++ b/Test/private/mocks/clearProjectV2ItemFieldValue.json @@ -0,0 +1,117 @@ +{ + "data": { + "updateProjectV2ItemFieldValue": { + "projectV2Item": { + "content": { + "__typename": "DraftIssue", + "id": "DI_lADOBCrGTM4ActQazgFYYPQ", + "body": "", + "title": "kk text" + }, + "id": "PVTI_lADOBCrGTM4ActQazgMuXXc", + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldUserValue", + "users": { + "nodes": [ + { + "login": "rulasg" + } + ] + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOBCrGTM4ActQazgSkYnA", + "name": "Assignees", + "dataType": "ASSIGNEES" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "kk text", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOBCrGTM4ActQazgSkYm8", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "Todo", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOBCrGTM4ActQazgSkYnE", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "Nice⭐️", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOBCrGTM4ActQazgSl5ME", + "name": "Severity", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "2d190e20", + "name": "Critical⭐️⭐️⭐️⭐️" + }, + { + "id": "94d0bf94", + "name": "Important⭐️⭐️⭐️" + }, + { + "id": "ea848251", + "name": "Needed⭐️⭐️" + }, + { + "id": "c99f6d55", + "name": "Nice⭐️" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldNumberValue", + "number": 8.0, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOBCrGTM4ActQazgSl5WM", + "name": "UserStories", + "dataType": "NUMBER" + } + }, + { + "__typename": "ProjectV2ItemFieldNumberValue", + "number": 666.0, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOBCrGTM4ActQazgSkglc", + "name": "TimeTracker", + "dataType": "NUMBER" + } + } + ] + } + } + } + } + } \ No newline at end of file diff --git a/Test/public/project_items_staged.test.ps1 b/Test/public/project_items_staged.test.ps1 index bed6084..824f33e 100644 --- a/Test/public/project_items_staged.test.ps1 +++ b/Test/public/project_items_staged.test.ps1 @@ -96,6 +96,59 @@ function Test_CommitProjectItemsStaged_SUCCESS{ Assert-AreEqual -Expected $fileTitleValue2 -Presented $item2.$fieldTitle2 } +function Test_CommitProjectItemsStaged_SUCCESS_Emptyfield{ + Reset-InvokeCommandMock + Mock_DatabaseRoot + + $Owner = "SomeOrg" ; $ProjectNumber = 164 + + # Item id 10 + # Name Value + # ---- ----- + # id PVTI_lADOBCrGTM4ActQazgMuXXc + # number + # Severity Nice⭐️ + # Status Todo + # TimeTracker 890 + # Comment This + # body some content in body + # Assignees rulasg + # UserStories 8 + # Title A draft in the project + # Priority 🥵High + # url + # type DraftIssue + + $itemId1 = "PVTI_lADOBCrGTM4ActQazgMuXXc" + $fieldComment1 = "Comment" + $fieldPriority1 = "Priority" + + # Edit-ProjectItem will call Get-Project with SkipItems + # This test is to confirm the sync works with the project and items + # Cache the project with items + MockCall_GitHubOrgProjectWithFields -Owner $owner -ProjectNumber $projectNumber -FileName 'projectV2.json' + $null = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber + + MockCallJson -FileName 'updateProjectV2ItemFieldValue.json' -Command 'Invoke-GitHubClearItemValues -ProjectId PVT_kwDOBCrGTM4ActQa -ItemId PVTI_lADOBCrGTM4ActQazgMuXXc -FieldId PVTF_lADOBCrGTM4ActQazgSl5GU' + MockCallJson -FileName 'updateProjectV2ItemFieldValue.json' -Command 'Invoke-GitHubClearItemValues -ProjectId PVT_kwDOBCrGTM4ActQa -ItemId PVTI_lADOBCrGTM4ActQazgMuXXc -FieldId PVTSSF_lADOBCrGTM4ActQazgSl5LY' + + Edit-ProjectItem -Owner $owner -ProjectNumber $projectNumber $itemId1 $fieldComment1 "" + Edit-ProjectItem -Owner $owner -ProjectNumber $projectNumber $itemId1 $fieldPriority1 "" + + $result = Sync-ProjectItemStaged -Owner $Owner -ProjectNumber $ProjectNumber + + # Return true + Assert-IsTrue -Condition $result + + # Staged list is empty + $staged = Get-ProjectItemStaged -Owner $Owner -ProjectNumber $ProjectNumber + Assert-IsNull -Object $staged + + $item1 = Get-ProjectItem -Owner $Owner -ProjectNumber $ProjectNumber -ItemId $itemId1 + Assert-StringIsNullOrEmpty -Presented $item1.$fieldComment1 + Assert-StringIsNullOrEmpty -Presented $item1.$fieldPriority1 +} + function Test_CommitProjectItemsStagedAsync_SUCCESS{ Reset-InvokeCommandMock Mock_DatabaseRoot @@ -339,4 +392,64 @@ function Test_CommitProjectItemsStagedAsync_debug{ Assert-NotImplemented +} + +function Test_Sync_ProjectDatabaseAsync_ClearValues{ + Reset-InvokeCommandMock + Mock_DatabaseRoot + + $Owner = "SomeOrg" ; $ProjectNumber = 164 + + $moduleRootPath = $PSScriptRoot | Split-Path -Parent | Split-Path -Parent | Convert-Path + + $projectId = "PVT_kwDOBCrGTM4ActQa" + $itemId1 = "PVTI_lADOBCrGTM4ActQazgMuXXc" + $fieldComment1 = "Comment" ; $fieldComment1Id = "PVTF_lADOBCrGTM4ActQazgSl5GU" + $fieldPriority1 = "Priority" ; $fieldPriority1Id ="PVTSSF_lADOBCrGTM4ActQazgSl5LY" + + # Edit-ProjectItem will call Get-Project with SkipItems + # This test is to confirm the sync works with the project and items + # Cache the project with items + MockCall_GitHubOrgProjectWithFields -Owner $owner -ProjectNumber $projectNumber -FileName 'projectV2.json' + $null = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber + + # Mock clear command for empty comment field (using async alias) + $clearCommand = 'Import-Module {projecthelper} ; Invoke-GitHubClearItemValues -ProjectId {ProjectId} -ItemId {ItemId} -FieldId {FieldId}' + $clearCommand = $clearCommand -replace '{projecthelper}', $moduleRootPath + $clearCommand = $clearCommand -replace '{ProjectId}', $projectId + $clearCommand = $clearCommand -replace '{ItemId}', $itemId1 + $clearCommand = $clearCommand -replace '{FieldId}', $fieldComment1Id + MockCallJsonAsync -Command $clearCommand -FileName "clearProjectV2ItemFieldValue.json" + + # Mock clear command for empty priority field (using async alias) + $clearCommand = 'Import-Module {projecthelper} ; Invoke-GitHubClearItemValues -ProjectId {ProjectId} -ItemId {ItemId} -FieldId {FieldId}' + $clearCommand = $clearCommand -replace '{projecthelper}', $moduleRootPath + $clearCommand = $clearCommand -replace '{ProjectId}', $projectId + $clearCommand = $clearCommand -replace '{ItemId}', $itemId1 + $clearCommand = $clearCommand -replace '{FieldId}', $fieldPriority1Id + MockCallJsonAsync -Command $clearCommand -FileName "clearProjectV2ItemFieldValue.json" + + # Stage the values - clear comment (empty string) and update title + Edit-ProjectItem -Owner $owner -ProjectNumber $projectNumber $itemId1 $fieldComment1 "" + Edit-ProjectItem -Owner $owner -ProjectNumber $projectNumber $itemId1 $fieldPriority1 "" + + Start-MyTranscript + $result = Sync-ProjectItemStagedAsync -Owner $Owner -ProjectNumber $ProjectNumber -SyncBatchSize 2 + $transcript = Stop-MyTranscript + + # Return true + Assert-IsTrue -Condition $result + + # Verify clear and update commands were called by checking mock invocations + # The transcript should show both operations + + # Staged list should be empty after successful sync + $staged = Get-ProjectItemStaged -Owner $Owner -ProjectNumber $ProjectNumber + Assert-IsNull -Object $staged + + # Verify the values in the database + $item1 = Get-ProjectItem -Owner $Owner -ProjectNumber $ProjectNumber -ItemId $itemId1 + Assert-StringIsNullOrEmpty -Presented $item1.$fieldComment1 + Assert-StringIsNullOrEmpty -Presented $item1.$fieldPriority1 + } \ No newline at end of file diff --git a/helper/module.helper.ps1 b/helper/module.helper.ps1 index 8eea760..1723a65 100644 --- a/helper/module.helper.ps1 +++ b/helper/module.helper.ps1 @@ -54,7 +54,7 @@ function Get-Ps1FullPath{ ) # If folderName is not empty - if($FolderName -ne $null){ + if($null -ne $FolderName){ $folder = Get-ModuleFolder -FolderName $FolderName -ModuleRootPath $ModuleRootPath $path = $folder | Join-Path -ChildPath $Name } else { @@ -102,6 +102,27 @@ function Get-ModuleName{ return $MODULE_NAME } +<# +.SYNOPSIS +Gets the full path of a specific module folder. + +.DESCRIPTION +Gets the full path of a specific module folder based on the folder name and optionally the module root path. + +.PARAMETER FolderName +The name of the folder to get the path for. Must be one of the valid folder names. + +.PARAMETER ModuleRootPath +The root path of the module. If not provided, uses the default module root path. + +.EXAMPLE +Get-ModuleFolder -FolderName "Public" +Gets the path to the Public folder of the current module. + +.EXAMPLE +Get-ModuleFolder -FolderName "Private" -ModuleRootPath "C:\MyModule" +Gets the path to the Private folder of the specified module. +#> function Get-ModuleFolder{ [CmdletBinding()] param( diff --git a/private/projectDatabase/project_database_Async.ps1 b/private/projectDatabase/project_database_Async.ps1 index 98748a4..a4fa9b8 100644 --- a/private/projectDatabase/project_database_Async.ps1 +++ b/private/projectDatabase/project_database_Async.ps1 @@ -1,4 +1,5 @@ Set-MyInvokeCommandAlias -Alias GitHub_UpdateProjectV2ItemFieldValueAsync -Command 'Import-Module {projecthelper} ; Invoke-GitHubUpdateItemValues -ProjectId {projectid} -ItemId {itemid} -FieldId {fieldid} -Value "{value}" -Type {type}' +Set-MyInvokeCommandAlias -Alias GitHub_ClearProjectV2ItemFieldValueAsync -Command 'Import-Module {projecthelper} ; Invoke-GitHubClearItemValues -ProjectId {projectid} -ItemId {itemid} -FieldId {fieldid}' function Sync-ProjectDatabaseAsync{ [CmdletBinding(SupportsShouldProcess)] @@ -136,7 +137,11 @@ function Sync-ProjectAsync{ "Calling to save [$projectId/$itemId/$fieldId ($type) = $value ]" | Write-MyHost - $job = Start-MyJob -Command GitHub_UpdateProjectV2ItemFieldValueAsync -Parameters $params + if([string]::IsNullOrWhiteSpace($value)){ + $job = Start-MyJob -Command GitHub_ClearProjectV2ItemFieldValueAsync -Parameters $params + } else { + $job = Start-MyJob -Command GitHub_UpdateProjectV2ItemFieldValueAsync -Parameters $params + } $call = [PSCustomObject]@{ job = $job @@ -210,56 +215,4 @@ function Waiting($Calls){ "" | Write-MyHost "Completed [$completed] Failed [$failed]" | Write-MyHost -} - -function Sync-Project{ - [CmdletBinding()] - param( - [Parameter(Position = 0)][object]$Database - ) - - $db = $Database - - foreach($idemId in $db.Staged.Keys){ - foreach($fieldId in $db.Staged.$idemId.Keys){ - - # Get actual value on the database - $fieldName = $db.fields.$fieldId.name - - # Skip if database has already the same value as staged - if($db.items.$itemId.$fieldName -eq $db.Staged.$itemId.$fieldId.Value){ - "Skipping [$itemId/$fieldName] as actual value is the same as staged value [$actualValue]" | Write-MyHost - continue - } - - $project_id = $db.ProjectId - $item_id = $idemId - $field_id = $fieldId - $value = $db.Staged.$idemId.$fieldId.Value - $type = ConvertTo-UpdateType $db.Staged.$idemId.$fieldId.Field.dataType - - $params = @{ - projectid = $project_id - itemid = $item_id - fieldid = $field_id - value = $value - type = $type - } - - "Saving [$project_id/$item_id/$field_id ($type) = $value ]" | Write-MyHost - - $result = Invoke-MyCommand -Command GitHub_UpdateProjectV2ItemFieldValue -Parameters $params - - if ($null -eq $result) { - "Updating Project Item Field [$item_id/$field_id/$value]" | Write-MyError - return $null - } - - # update database with change - $db.items.$item_id.$fieldName = $value - - } - } - - return $db } \ No newline at end of file diff --git a/private/projectDatabase/project_database_Sync.ps1 b/private/projectDatabase/project_database_Sync.ps1 index caf76f9..eea9a81 100644 --- a/private/projectDatabase/project_database_Sync.ps1 +++ b/private/projectDatabase/project_database_Sync.ps1 @@ -1,5 +1,6 @@ Set-MyInvokeCommandAlias -Alias GitHub_UpdateProjectV2ItemFieldValue -Command 'Invoke-GitHubUpdateItemValues -ProjectId {projectid} -ItemId {itemid} -FieldId {fieldid} -Value "{value}" -Type {type}' +Set-MyInvokeCommandAlias -Alias GitHub_ClearProjectV2ItemFieldValue -Command 'Invoke-GitHubClearItemValues -ProjectId {projectid} -ItemId {itemid} -FieldId {fieldid}' function Sync-ProjectDatabase{ [CmdletBinding(SupportsShouldProcess)] @@ -102,7 +103,11 @@ function Sync-Project{ "Saving [$project_id/$item_id/$field_id ($type) = $value ] ..." | Write-MyHost -NoNewLine - $result = Invoke-MyCommand -Command GitHub_UpdateProjectV2ItemFieldValue -Parameters $params + if([string]::IsNullOrWhiteSpace($value)){ + $result = Invoke-MyCommand -Command GitHub_ClearProjectV2ItemFieldValue -Parameters $params + } else { + $result = Invoke-MyCommand -Command GitHub_UpdateProjectV2ItemFieldValue -Parameters $params + } if ($null -eq $result) { "FAILED !!" | Write-MyHost diff --git a/public/driver/issue/Invoke-FindProject.ps1 b/public/driver/issue/Invoke-FindProject.ps1 index dae1ca5..48ab3f3 100644 --- a/public/driver/issue/Invoke-FindProject.ps1 +++ b/public/driver/issue/Invoke-FindProject.ps1 @@ -22,26 +22,26 @@ function Invoke-FindProject{ <# .SYNOPSIS Finds GitHub projects for a specified organization owner. - + .DESCRIPTION Uses the GitHub GraphQL API to search for projects matching a given pattern within an organization. This is an integration function not intended for direct user use. - + .PARAMETER Owner The GitHub organization name to search within. - + .PARAMETER Pattern Optional pattern to filter projects by name. - + .PARAMETER firstProject Number of projects to return in a single request. Default is 100. - + .PARAMETER afterProject Pagination cursor for subsequent requests. Default is null. - + .OUTPUTS Returns the GraphQL response object containing project information or null if an error occurs. - + .NOTES This function requires GitHub authentication via the gh CLI. #> diff --git a/public/driver_gh.ps1 b/public/driver_gh.ps1 index 2a9eaba..bc8f704 100644 --- a/public/driver_gh.ps1 +++ b/public/driver_gh.ps1 @@ -214,6 +214,76 @@ function Invoke-GitHubUpdateItemValues{ return $response } Export-ModuleMember -Function Invoke-GitHubUpdateItemValues +<# +.SYNOPSIS +Clears item values in a GitHub project + +.DESCRIPTION +This is an integration function that is not intended to be used directly by the user. +Clears the values of specific fields for an item in a GitHub project. + +.NOTES +Currently only text, number, date, assignees, labels, single-select, iteration and milestone fields are supported. +#> +function Invoke-GitHubClearItemValues{ + param( + [Parameter(Mandatory=$true)] [string]$ProjectId, + [Parameter(Mandatory=$true)] [string]$ItemId, + [Parameter(Mandatory=$true)] [string]$FieldId + ) + + # Use the environmentraviable + $token = Get-GithubToken + if(-not $token){ + throw "GH Cli Auth Token not available. Run 'gh auth login' in your terminal." + } + + # Define the GraphQL query with variables + $qlPath = $PSScriptRoot | Join-Path -ChildPath "graphql" -AdditionalChildPath "clearItemValues.mutant" + $mutation = get-content -path $qlPath | Out-String + + # Define the headers for the request + $headers = @{ + "Authorization" = "Bearer $token" + "Content-Type" = "application/json" + } + + # Ensure that if the $type is number the value is a number + # API fails if when updaring a number the value type in the Input payload s not a number + if($Type -eq "number"){ + $Value = [decimal]$Value + } + + # Define the variables for the request + $variables = @{ + input = @{ + projectId = $ProjectId + itemId = $ItemId + fieldId = $FieldId + } + } + + # Define the body for the request + $body = @{ + query= $mutation + variables = $variables + } | ConvertTo-Json -Depth 10 + + # Send the request + $response = Invoke-RestMethod -Uri 'https://api.github.com/graphql' -Method Post -Body $body -Headers $headers + + # Check if here are errors + if($response.errors){ + $response.errors | ForEach-Object { + "RESPONSE Type[$($_.type)] $($_.message)" | Write-MyError + } + return $null + } + + # Return the field names + return $response +} Export-ModuleMember -Function Invoke-GitHubClearItemValues + <# .SYNOPSIS Gets issue or pull request information diff --git a/public/getPrompt.ps1 b/public/getPrompt.ps1 index 8248b64..8249d7e 100644 --- a/public/getPrompt.ps1 +++ b/public/getPrompt.ps1 @@ -6,7 +6,7 @@ This file contains functions to control ProjectHelper prompt. function Initialize-ProjectHelperPromptSettings { [CmdletBinding()] - param( + param( [switch]$Force ) diff --git a/public/graphql/clearItemValues.mutant b/public/graphql/clearItemValues.mutant new file mode 100644 index 0000000..c2235f4 --- /dev/null +++ b/public/graphql/clearItemValues.mutant @@ -0,0 +1,90 @@ +mutation ClearItemValues($input:ClearProjectV2ItemFieldValueInput!){clearProjectV2ItemFieldValue(input:$input){projectV2Item{content{__typename,... on DraftIssue{id,body,title + },... on PullRequest{body,title,number,url,repository{nameWithOwner + } + },... on Issue{body,title,number,url,repository{nameWithOwner + } + } + },id,fieldValues(first: 100){nodes{__typename,... on ProjectV2ItemFieldDateValue{date,field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldIterationValue{title,startDate,duration,field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldLabelValue{labels(first: 10){nodes{name + } + },field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldNumberValue{number,field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldSingleSelectValue{name,field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldTextValue{text,field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldMilestoneValue{milestone{title,description,dueOn + },field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldPullRequestValue{pullRequests(first: 10){nodes{url + } + },field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldRepositoryValue{repository{url + },field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldUserValue{users(first: 10){nodes{login + } + },field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + },... on ProjectV2ItemFieldReviewerValue{reviewers(first: 10){nodes{__typename,... on Team{name + },... on User{login + } + } + },field{__typename,... on ProjectV2Field{id,name,dataType + },... on ProjectV2IterationField{id,name,dataType + },... on ProjectV2SingleSelectField{id,name,dataType,options{id,name + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/public/project_fields_list.ps1 b/public/project_fields_list.ps1 index b92828c..d2f11af 100644 --- a/public/project_fields_list.ps1 +++ b/public/project_fields_list.ps1 @@ -1,3 +1,35 @@ +<# +.SYNOPSIS +Gets the list of fields available in a GitHub project. + +.DESCRIPTION +Retrieves all custom fields defined in a GitHub project and returns them formatted for display. +Can optionally filter fields by name using wildcard matching. + +.PARAMETER Owner +The owner of the GitHub repository containing the project. + +.PARAMETER ProjectNumber +The project number in the repository. + +.PARAMETER Name +Optional filter to search for fields containing the specified text in their name. + +.PARAMETER Force +Forces a refresh of the project data from GitHub. + +.OUTPUTS +System.Object[] +Returns an array of project field objects with name, dataType, and additional information. + +.EXAMPLE +Get-ProjectFields -Owner "octocat" -ProjectNumber "1" +Gets all fields from project 1 in the octocat repository. + +.EXAMPLE +Get-ProjectFields -Owner "octocat" -ProjectNumber "1" -Name "status" +Gets all fields from project 1 that contain "status" in their name. +#> function Get-ProjectFields{ [CmdletBinding()] [OutputType([string[]])] diff --git a/public/project_item.ps1 b/public/project_item.ps1 index f1d4cf2..380e63a 100644 --- a/public/project_item.ps1 +++ b/public/project_item.ps1 @@ -89,30 +89,30 @@ function Add-ProjectItem{ ($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 $projectId = Get-ProjectId -Owner $Owner -ProjectNumber $ProjectNumber if(-not $projectId){ "Project ID not found for Owner [$Owner] and ProjectNumber [$ProjectNumber]" | Write-MyError return $null } - + # Get item id $contentId = Get-ContentIdFromUrl -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 } - + # check if the response is null if($response.errors){ "[$($response.errors[0].type)] $($response.errors[0].message)" | Write-MyError return $null } - + if($response.data.addProjectV2ItemById.item.id) { return $response.data.addProjectV2ItemById.item.id diff --git a/public/project_item_List.ps1 b/public/project_item_List.ps1 index aba665d..b2ab818 100644 --- a/public/project_item_List.ps1 +++ b/public/project_item_List.ps1 @@ -1,5 +1,40 @@ +<# +.SYNOPSIS +Gets a list of project items from a GitHub project. + +.DESCRIPTION +Retrieves all items from a GitHub project and returns them as a hashtable with ItemId as the key. +Can optionally exclude items with status "Done". + +.PARAMETER Owner +The owner of the GitHub repository containing the project. + +.PARAMETER ProjectNumber +The project number in the repository. + +.PARAMETER Project +An existing project object. If provided, Owner and ProjectNumber are not required. + +.PARAMETER ExcludeDone +When specified, excludes items with status "Done" from the results. + +.PARAMETER Force +Forces a refresh of the project data from GitHub. + +.OUTPUTS +System.Collections.Hashtable +Returns a hashtable where keys are ItemIds and values are project item objects. + +.EXAMPLE +Get-ProjectItemList -Owner "octocat" -ProjectNumber "1" +Gets all items from project 1 in the octocat organization. + +.EXAMPLE +Get-ProjectItemList -Owner "octocat" -ProjectNumber "1" -ExcludeDone +Gets all items from project 1 excluding those with status "Done". +#> function Get-ProjectItemList{ [CmdletBinding()] [OutputType([string[]])] @@ -61,6 +96,34 @@ function Test-ItemIsDone($Item){ return $ret } +<# +.SYNOPSIS +Finds project items by exact title match. + +.DESCRIPTION +Searches for project items that have an exact title match (case-insensitive). +Returns all items that match the specified title. + +.PARAMETER Title +The exact title to search for. The comparison is case-insensitive and trims whitespace. + +.PARAMETER Owner +The owner of the GitHub repository containing the project. + +.PARAMETER ProjectNumber +The project number in the repository. + +.PARAMETER Force +Forces a refresh of the project data from GitHub. + +.OUTPUTS +System.Object[] +Returns an array of project item objects that match the title. + +.EXAMPLE +Find-ProjectItemByTitle -Title "Bug Fix" -Owner "octocat" -ProjectNumber "1" +Finds all items in project 1 with the exact title "Bug Fix". +#> function Find-ProjectItemByTitle{ [CmdletBinding()] param( @@ -84,6 +147,34 @@ function Find-ProjectItemByTitle{ } Export-ModuleMember -Function Find-ProjectItemByTitle +<# +.SYNOPSIS +Searches for project items by title using wildcard matching. + +.DESCRIPTION +Searches for project items where the title contains the specified text using wildcard matching. +This is different from Find-ProjectItemByTitle which requires an exact match. + +.PARAMETER Owner +The owner of the GitHub repository containing the project. + +.PARAMETER ProjectNumber +The project number in the repository. + +.PARAMETER Title +The text to search for within item titles. Uses wildcard matching (*Title*). + +.PARAMETER Force +Forces a refresh of the project data from GitHub. + +.OUTPUTS +System.Object[] +Returns an array of project item objects that contain the specified title text. + +.EXAMPLE +Search-ProjectItemByTitle -Owner "octocat" -ProjectNumber "1" -Title "bug" +Searches for all items in project 1 that contain "bug" in their title. +#> function Search-ProjectItemByTitle{ [CmdletBinding()] param( @@ -107,6 +198,41 @@ function Search-ProjectItemByTitle{ } Export-ModuleMember -Function Search-ProjectItemByTitle +<# +.SYNOPSIS +Searches for project items using a filter across multiple fields. + +.DESCRIPTION +Searches for project items using a filter that can match across multiple fields including title, +comments, and other project item properties. Returns items formatted for display. + +.PARAMETER filter +The search filter to apply across project item fields. + +.PARAMETER Owner +The owner of the GitHub repository containing the project. + +.PARAMETER ProjectNumber +The project number in the repository. + +.PARAMETER Fields +An array of field names to display in the results. If not specified, uses environment default display fields. + +.PARAMETER Force +Forces a refresh of the project data from GitHub. + +.OUTPUTS +System.Object[] +Returns an array of formatted project item objects that match the filter. + +.EXAMPLE +Search-ProjectItem -filter "bug" -Owner "octocat" -ProjectNumber "1" +Searches for all items in project 1 that contain "bug" in any field. + +.EXAMPLE +Search-ProjectItem -filter "urgent" -Owner "octocat" -ProjectNumber "1" -Fields @("Title", "Status") +Searches for items containing "urgent" and displays only Title and Status fields. +#> function Search-ProjectItem{ [CmdletBinding()] param(