diff --git a/Test/private/MockCall_Project700.ps1 b/Test/private/MockCall_Project700.ps1 index 62079dd..8be1a0e 100644 --- a/Test/private/MockCall_Project700.ps1 +++ b/Test/private/MockCall_Project700.ps1 @@ -48,7 +48,7 @@ function Get-Mock_Project_700 { $project.title = $pActual.title $project.number = $pActual.number $project.url = $pActual.url - $project.cacheFileName = "$($pActual.owner.login)_$($pActual.number).json" + $project.cacheFileName = "db-$($pActual.owner.login)-$($pActual.number)-project.json" # Fields info $project.fields = @{} diff --git a/Test/private/mocks/invoke-getitem-PVTI_lADOAlIw4c4BCe3Vzgeio4o-Normalized.json b/Test/private/mocks/invoke-getitem-PVTI_lADOAlIw4c4BCe3Vzgeio4o-Normalized.json new file mode 100644 index 0000000..f25a1af --- /dev/null +++ b/Test/private/mocks/invoke-getitem-PVTI_lADOAlIw4c4BCe3Vzgeio4o-Normalized.json @@ -0,0 +1,185 @@ +{ + "data": { + "node": { + "id": "PVTI_lADOAlIw4c4BCe3Vzgeio4o", + "type": "ISSUE", + "fullDatabaseId": "128099210", + "project": { + "id": "PVT_kwDOAlIw4c4BCe3V", + "url": "https://github.com/orgs/octodemo/projects/700" + }, + "content": { + "__typename": "Issue", + "id": "I_kwDOPrRnkc7KkwSq", + "body": "Body of issue for development", + "title": "[RULaSG-DeV-1] Issue [RULASG-DEV-1] for [value between] development", + "updatedAt": "2025-10-15T21:30:02Z", + "createdAt": "2025-09-09T14:01:17Z", + "number": 26, + "url": "https://github.com/octodemo/rulasg-dev-1/issues/26", + "state": "OPEN", + "repository": { + "name": "rulasg-dev-1", + "owner": { + "login": "octodemo" + } + }, + "comments": { + "totalCount": 3, + "nodes": [ + { + "createdAt": "2025-09-23T17:51:06Z", + "updatedAt": "2025-10-15T21:29:45Z", + "url": "https://github.com/octodemo/rulasg-dev-1/issues/26#issuecomment-3324995787", + "body": "Sample comment 1", + "fullDatabaseId": "3324995787", + "author": { + "login": "rulasg" + } + }, + { + "createdAt": "2025-09-24T08:29:13Z", + "updatedAt": "2025-10-15T21:29:55Z", + "url": "https://github.com/octodemo/rulasg-dev-1/issues/26#issuecomment-3327194303", + "body": "Sample comment 2", + "fullDatabaseId": "3327194303", + "author": { + "login": "rulasg" + } + }, + { + "createdAt": "2025-09-30T05:42:49Z", + "updatedAt": "2025-10-15T21:30:02Z", + "url": "https://github.com/octodemo/rulasg-dev-1/issues/26#issuecomment-3350059109", + "body": "Sample comment 3", + "fullDatabaseId": "3350059109", + "author": { + "login": "rulasg" + } + } + ] + } + }, + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldRepositoryValue", + "repository": { + "url": "https://github.com/octodemo/rulasg-dev-1" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-k", + "name": "Repository", + "dataType": "REPOSITORY" + } + }, + { + "__typename": "ProjectV2ItemFieldMilestoneValue", + "milestone": { + "title": "Milestone 3: Quality and Deployment", + "description": "Testing, documentation, and deployment pipeline setup", + "dueOn": "2025-10-18T00:00:00Z" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-g", + "name": "Milestone", + "dataType": "MILESTONE" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "[RULaSG-DeV-1] Issue [RULASG-DEV-1] for [value between] development", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "In Progress", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldNumberValue", + "number": 333.0, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhjU", + "name": "field-number", + "dataType": "NUMBER" + } + }, + { + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "field-iteration 3", + "startDate": "2025-10-05", + "duration": 14, + "field": { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "text3", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "option-3", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + } + } + ] + } + } + } +} diff --git a/Test/public/items/edit_project_item.test.ps1 b/Test/public/items/edit_project_item.test.ps1 index 99815d0..73fc2f5 100644 --- a/Test/public/items/edit_project_item.test.ps1 +++ b/Test/public/items/edit_project_item.test.ps1 @@ -263,6 +263,57 @@ function Test_EditProjectItems_NormalizeTitle{ Assert-AreEqual -Expected $newTitle -Presented $result.$itemId.title.Value } +function Test_EditProjectItems_NormalizeTitle_AlreadyNormalized{ + + # Arrange + $p = Get-Mock_Project_700 ; $Owner = $p.owner ; $ProjectNumber = $p.number + MockCall_GetProject $p -skipItems + $i= $p.issue ; $itemId = $i.id + + MockCall_GetProject $p -SkipItems + + $newTitle = "[rulasg-dev-1] Issue [rulasg-dev-1] for [value between] development" + + # Mock the direct call for item + MockCallJson -Command "Invoke-GetItem -itemid $itemId" -FileName "invoke-getitem-$itemId-Normalized.json" + # Act + Edit-ProjectItem -Owner $owner -ProjectNumber $projectNumber -ItemId $itemId -NormalizeTitle + + # Assert + $result = Get-ProjectItemStaged -Owner $owner -ProjectNumber $projectNumber + + Assert-Count -Expected 1 -Presented $result.$itemId.Keys + Assert-AreEqual -Expected $newTitle -Presented $result.$itemId.title.Value +} + +function Test_NormalizedTitle{ + + Invoke-PrivateContext{ + $cases = @( + @{item = @{Title= "Test"; repositoryName = "rulasg-dev-1"}; expected = "[rulasg-dev-1] Test"} + @{item = @{Title= "[rulasg-dev-1] Test"; repositoryName = "rulasg-dev-1"}; expected = "[rulasg-dev-1] Test"} + + @{item = @{Title= "[BBVA] Test"; repositoryName = "bBva"}; expected = "[bBva] Test"} + + @{item = @{Title= "Test [rulasg-dev-1]"; repositoryName = "rulasg-dev-1"}; expected = "Test [rulasg-dev-1]"} + + @{item = @{Title= "Test [rulasg-dEv-1]"; repositoryName = "rulasg-dev-1"}; expected = "Test [rulasg-dev-1]"} + + @{item = @{Title= "[RULaSG-DeV-1] Test"; repositoryName = "rulasg-dev-1"}; expected = "[rulasg-dev-1] Test"} + @{item = @{Title= "[RULaSG-DeV-1] Test [value between] development"; repositoryName = "rulasg-dev-1"}; ` + expected = "[rulasg-dev-1] Test [value between] development"} + @{item = @{Title= "[RULaSG-DeV-1] Issue [RULASG-DEV-1] for [value between] development"; repositoryName = "rulasg-dev-1"}; ` + expected = "[rulasg-dev-1] Issue [rulasg-dev-1] for [value between] development"} + ) + + foreach($case in $cases){ + $result = Get-NormalizedTitle -Item $case.item + Assert-AreEqual -Expected $case.expected -Presented $result + } + + } +} + function Test_EditProjectItems_OpenInBrowser{ # Arrange diff --git a/private/database/databaseV2.ps1 b/private/database/databaseV2.ps1 index f01301e..b4b6252 100644 --- a/private/database/databaseV2.ps1 +++ b/private/database/databaseV2.ps1 @@ -95,6 +95,29 @@ function Save-Database { $Database | ConvertTo-Json -Depth 10 | Set-Content $path } +function Get-DatabaseKey{ + [CmdletBinding()] + param( + [Parameter(Position = 0)][string]$Owner, + [Parameter(Position = 1)][int]$ProjectNumber, + [Parameter(Position = 2)][string] $Category + ) + + if([string]::IsNullOrWhiteSpace($Owner)){ + throw "Owner is null or empty" + } + if($ProjectNumber -le 0){ + throw "ProjectNumber is null or not a positive integer" + } + if([string]::IsNullOrWhiteSpace($Category)){ + throw "Category is null or empty" + } + + $ret = "db-{0}-{1}-{2}" -f $Owner, $ProjectNumber, $Category + + return $ret +} + function Get-DatabaseFile { [CmdletBinding()] param( diff --git a/private/projectDatabase/project_database.ps1 b/private/projectDatabase/project_database.ps1 index 9a60638..d725b55 100644 --- a/private/projectDatabase/project_database.ps1 +++ b/private/projectDatabase/project_database.ps1 @@ -7,7 +7,7 @@ function Test-ProjectDatabase{ [Parameter(Position = 1)][int]$ProjectNumber ) - $key = Get-DatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber + $key = Get-ProjectDatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber $ret = Test-Database -Key $key @@ -21,7 +21,7 @@ function Test-ProjectDatabaseStaged{ [Parameter(Position = 1)][int]$ProjectNumber ) - $key = Get-DatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber + $key = Get-ProjectDatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber $prj = Get-Database -Key $key if($null -eq $prj){ @@ -46,7 +46,7 @@ function Get-ProjectFromDatabase{ [Parameter(Position = 1)][int]$ProjectNumber ) - $key = Get-DatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber + $key = Get-ProjectDatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber $prj = Get-Database -Key $key if($null -eq $prj){ @@ -67,7 +67,7 @@ function Reset-ProjectDatabase{ [Parameter(Position = 1)][int]$ProjectNumber ) - $dbKey = Get-DatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber + $dbKey = Get-ProjectDatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber Reset-Database -Key $dbKey } @@ -139,7 +139,7 @@ function Save-ProjectDatabase{ throw "Database.number is null or not a positive integer" } - $dbkey = Get-DatabaseKey -Owner $owner -ProjectNumber $projectnumber + $dbkey = Get-ProjectDatabaseKey -Owner $owner -ProjectNumber $projectnumber if($Safe){ $oldDatabase = Get-Database -Key $dbkey @@ -156,21 +156,12 @@ function Save-ProjectDatabase{ Save-Database -Key $dbkey -Database $Database } -function Get-DatabaseKey{ +function Get-ProjectDatabaseKey{ [CmdletBinding()] param( [Parameter(Position = 0)][string]$Owner, [Parameter(Position = 1)][int]$ProjectNumber ) - if([string]::IsNullOrWhiteSpace($Owner)){ - throw "Owner is null or empty" - } - if($ProjectNumber -le 0){ - throw "ProjectNumber is null or not a positive integer" - } - - $ret = "$($owner)_$($projectnumber)" - - return $ret + return Get-DatabaseKey $Owner $ProjectNumber "project" } \ No newline at end of file diff --git a/public/fields/project_fields_list.ps1 b/public/fields/project_fields_list.ps1 index ba16601..73a3b46 100644 --- a/public/fields/project_fields_list.ps1 +++ b/public/fields/project_fields_list.ps1 @@ -118,10 +118,17 @@ function getFieldsCache{ ) $key = "$Owner-$ProjectNumber" - $ret = $script:fieldsCache[$key] + $lockKey = Get-DatabaseKey $Owner $ProjectNumber "field-cachelock" - return $ret + $lock = Get-Database -Key $lockKey + $cache = $script:fieldsCache[$key] + if($lock -cne $cache.SafeId) { + $script:fieldsCache.Remove($key) + return $null + } + + return $cache.List } function setFieldsCache{ @@ -132,5 +139,16 @@ function setFieldsCache{ ) $key = "$Owner-$ProjectNumber" - $script:fieldsCache[$key] = $FieldList + $lockKey = Get-DatabaseKey $Owner $ProjectNumber "field-cachelock" + + $safeId = [Guid]::NewGuid().ToString() + + # Save safeId to field-lock + Save-Database -Database $safeId -Key $lockKey + + # Set lock in database to prevent concurrent updates + $script:fieldsCache[$key] = @{ + List = $FieldList + SafeId = $safeId + } } \ No newline at end of file diff --git a/public/integrations/update-ProjectItemsWithIntegration.ps1 b/public/integrations/update-ProjectItemsWithIntegration.ps1 index 9818884..2c3c1f7 100644 --- a/public/integrations/update-ProjectItemsWithIntegration.ps1 +++ b/public/integrations/update-ProjectItemsWithIntegration.ps1 @@ -24,9 +24,6 @@ function Update-ProjectItemsWithIntegration{ ) ($Owner,$ProjectNumber) = Resolve-ProjectParameters -Owner $Owner -ProjectNumber $ProjectNumber - # Sync project if needed - $null = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber -Force:$Force - $params = @{ Owner = $Owner ProjectNumber = $ProjectNumber diff --git a/public/items/edit_project_item.ps1 b/public/items/edit_project_item.ps1 index 7b6988a..4fed57a 100644 --- a/public/items/edit_project_item.ps1 +++ b/public/items/edit_project_item.ps1 @@ -80,16 +80,6 @@ function Edit-ProjectItem { $AddComment = Get-LongText -Text $AddComment } - # NormalizeTitle - if ($NormalizeTitle) { - if([string]::IsNullOrWhiteSpace($Title)){ - $Title = "[{{RepositoryName}}] {{Title}}" - } else { - # Editing Title at the same time - $Title = "[{{RepositoryName}}] $Title" - } - } - # Default if($DefaultValues){ Write-MyWarning "No Default values are currently setup. Please use other parameters to set values or update the DefaultValues parameter with default values" @@ -174,6 +164,8 @@ function Edit-ProjectItem { } } + # With Item + if ($OpenInBrowser) { $item = Get-ProjectItem -ItemId $Id -Owner $Owner -ProjectNumber $ProjectNumber if ($null -ne $item) { @@ -182,6 +174,14 @@ function Edit-ProjectItem { Write-Warning "Item not found. Cannot open in browser." } } + + # NormalizeTitle + if ($NormalizeTitle) { + $item = Get-ProjectItem -ItemId $Id -Owner $Owner -ProjectNumber $ProjectNumber + $params.fieldname = "Title" + $params.value = Get-NormalizedTitle -Item $item + edit $params + } } end { @@ -243,4 +243,43 @@ function Edit-ProjectItemValue { Save-ProjectDatabaseSafe -Database $db } -} \ No newline at end of file +} + +# function Get-NormalizedTitle { +# param( +# [Parameter(Mandatory)][hashtable]$Item +# ) +# $title = $Item.Title +# $header = "[{0}]" -f $Item.RepositoryName + +# if($title.ToLower().Contains($header.ToLower())){ +# "The title is already normalized" | Write-MyDebug -section "Edit-ProjectItem" +# # update title with proper repository name case +# $newTitle = $title -replace "^\[[^\]]*\]\s*", "$header" +# return $newTitle +# } else { +# return "$header $title" +# } +# } + +function Get-NormalizedTitle { + param( + [Parameter(Mandatory)][hashtable]$Item + ) + $title = $Item.Title + $header = "[{0}]" -f $Item.RepositoryName + $repoEscaped = [regex]::Escape($Item.RepositoryName) + + "Original title: $title" | Write-MyDebug -section "Edit-ProjectItem NormalizedTitle" + + if($title -imatch "\[$repoEscaped\]" -or $title -imatch "^\s*\[?$repoEscaped\]\s*"){ + # Ttile contains repo name. Normalize it to proper case and formatting. This will cover the following scenarios: + $ret = ($title -ireplace "^\s*\[?$repoEscaped\]\s*", "$header ") -ireplace "\[$repoEscaped\]", $header + + } else { + $ret = "$header $title" + } + + "Normalized title: $ret" | Write-MyDebug -section "Edit-ProjectItem NormalizedTitle" + return $ret +} \ No newline at end of file diff --git a/public/items/project_item.ps1 b/public/items/project_item.ps1 index a8aaa4f..cd11a01 100644 --- a/public/items/project_item.ps1 +++ b/public/items/project_item.ps1 @@ -733,15 +733,15 @@ function Test-WhereExactField { function AreEqual { param( - [object]$Object1, - [object]$Object2 + [string]$Object1, + [string]$Object2 ) $Object1 = [string]::IsNullOrEmpty($Object1) ? $null : $Object1 $Object2 = [string]::IsNullOrEmpty($Object2) ? $null : $Object2 # Check if the objects are equal - $ret = $Object1 -eq $Object2 + $ret = $Object1 -ceq $Object2 return $ret } diff --git a/public/projectCache/projectCache.ps1 b/public/projectCache/projectCache.ps1 index 384a0ba..fad6049 100644 --- a/public/projectCache/projectCache.ps1 +++ b/public/projectCache/projectCache.ps1 @@ -24,7 +24,7 @@ function Get-ProjectCacheFile{ ($Owner, $ProjectNumber) = Resolve-ProjectParameters -Owner $Owner -ProjectNumber $ProjectNumber - $key = Get-DatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber + $key = Get-ProjectDatabaseKey -Owner $Owner -ProjectNumber $ProjectNumber $path = Get-DatabaseFile -Key $key if($path | Test-Path ){