mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-22 23:46:36 +00:00
fix: preserve selections when config import is invalid (#4994)
* fix: validate imports before replacing selections * test: cover atomic config imports * docs: explain stale config imports --------- Co-authored-by: Guilherme de Oliveira Rocha <contatoguilhermeoliveira@protonmail.com> Co-authored-by: Chris Titus <contact@christitus.com>
This commit is contained in:
co-authored by
Guilherme de Oliveira Rocha
Chris Titus
parent
2d0fd43bd3
commit
792122e998
@@ -40,6 +40,12 @@ This is useful for:
|
||||
- Reusing a known-good baseline after reinstalling Windows
|
||||
- Standardizing deployments for labs, workstations, or personal setups
|
||||
|
||||
:::caution[Keep exported configurations current]
|
||||
Exported configurations contain the WinUtil catalog keys that existed when the file was created. If a later WinUtil version removes or renames one of those keys, the import is rejected before any current selections are changed. PowerShell reports the stale entry as `Unknown selection key '<key>'`.
|
||||
|
||||
To recover, compare the reported key with the current files in the [WinUtil configuration catalog](https://github.com/ChrisTitusTech/winutil/tree/main/config). Remove or replace the stale key in your JSON file, or create and export a new configuration with the current WinUtil version, then run the import again. Re-export long-lived baselines after catalog changes so they remain compatible.
|
||||
:::
|
||||
|
||||
:::note
|
||||
Run the command in an elevated PowerShell session so WinUtil can apply system-level changes.
|
||||
:::
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
function Update-WinUtilSelections ($flatJson) {
|
||||
function Update-WinUtilSelections {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string[]]$flatJson,
|
||||
|
||||
[switch]$Replace
|
||||
)
|
||||
|
||||
$nextSelections = @{
|
||||
selectedApps = [System.Collections.Generic.List[string]]::new()
|
||||
selectedTweaks = [System.Collections.Generic.List[string]]::new()
|
||||
selectedToggles = [System.Collections.Generic.List[string]]::new()
|
||||
selectedFeatures = [System.Collections.Generic.List[string]]::new()
|
||||
selectedAppx = [System.Collections.Generic.List[string]]::new()
|
||||
}
|
||||
|
||||
foreach ($cbkey in $flatJson) {
|
||||
|
||||
$listName = switch -Regex ($cbkey) {
|
||||
@@ -9,6 +24,45 @@ function Update-WinUtilSelections ($flatJson) {
|
||||
'^WPFAppx' { 'selectedAppx' }
|
||||
}
|
||||
|
||||
$sync.$listName.Add($cbkey)
|
||||
if (-not $listName) {
|
||||
throw "Unsupported selection key '$cbkey'."
|
||||
}
|
||||
|
||||
$isKnownSelection = switch ($listName) {
|
||||
'selectedApps' {
|
||||
$sync.configs.applicationsHashtable.ContainsKey($cbkey)
|
||||
}
|
||||
'selectedTweaks' {
|
||||
$null -ne $sync.configs.tweaks.PSObject.Properties[$cbkey]
|
||||
}
|
||||
'selectedToggles' {
|
||||
$null -ne $sync.configs.tweaks.PSObject.Properties[$cbkey]
|
||||
}
|
||||
'selectedFeatures' {
|
||||
$null -ne $sync.configs.feature.PSObject.Properties[$cbkey]
|
||||
}
|
||||
'selectedAppx' {
|
||||
$sync.configs.appxHashtable.ContainsKey($cbkey)
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $isKnownSelection) {
|
||||
throw "Unknown selection key '$cbkey'."
|
||||
}
|
||||
|
||||
$nextSelections[$listName].Add($cbkey)
|
||||
}
|
||||
|
||||
if ($Replace) {
|
||||
foreach ($listName in $nextSelections.Keys) {
|
||||
$sync[$listName] = $nextSelections[$listName]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($listName in $nextSelections.Keys) {
|
||||
foreach ($cbkey in $nextSelections[$listName]) {
|
||||
$sync.$listName.Add($cbkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,15 +84,9 @@ function Invoke-WPFImpex {
|
||||
return
|
||||
}
|
||||
|
||||
# Clear all existing selections before importing so the import replaces
|
||||
# the current state rather than merging with it
|
||||
$sync.selectedAppx = [System.Collections.Generic.List[string]]::new()
|
||||
$sync.selectedApps = [System.Collections.Generic.List[string]]::new()
|
||||
$sync.selectedTweaks = [System.Collections.Generic.List[string]]::new()
|
||||
$sync.selectedToggles = [System.Collections.Generic.List[string]]::new()
|
||||
$sync.selectedFeatures = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
Update-WinUtilSelections -flatJson $flattenedJson
|
||||
# Build and validate every imported selection before replacing the current
|
||||
# state. This keeps a malformed config from leaving partial selections behind.
|
||||
Update-WinUtilSelections -flatJson $flattenedJson -Replace
|
||||
|
||||
if ($sync.Form) {
|
||||
Reset-WPFCheckBoxes -doToggles $true
|
||||
|
||||
+118
-2
@@ -48,12 +48,17 @@ namespace System.Windows.Controls
|
||||
|
||||
public class WrapPanel
|
||||
{
|
||||
public global::Windows.Visibility Visibility { get; set; }
|
||||
public object Visibility { get; set; }
|
||||
}
|
||||
|
||||
public class StackPanel
|
||||
{
|
||||
public System.Collections.ArrayList Children { get; } = new System.Collections.ArrayList();
|
||||
public System.Collections.ArrayList Children { get; private set; }
|
||||
|
||||
public StackPanel()
|
||||
{
|
||||
Children = new System.Collections.ArrayList();
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
@@ -61,6 +66,7 @@ namespace System.Windows.Controls
|
||||
|
||||
. (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\private\Reset-WPFCheckBoxes.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFImpex.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFGetInstalled.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFSelectedCheckboxesUpdate.ps1")
|
||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1")
|
||||
@@ -127,6 +133,16 @@ namespace System.Windows.Controls
|
||||
Content = "Git"
|
||||
}
|
||||
}
|
||||
appxHashtable = @{
|
||||
WPFAppxExample = [pscustomobject]@{}
|
||||
}
|
||||
tweaks = [pscustomobject]@{
|
||||
WPFTweaksTelemetry = [pscustomobject]@{}
|
||||
WPFToggleDarkMode = [pscustomobject]@{}
|
||||
}
|
||||
feature = [pscustomobject]@{
|
||||
WPFFeatureSandbox = [pscustomobject]@{}
|
||||
}
|
||||
}
|
||||
WPFselectedAppsButton = [pscustomobject]@{
|
||||
Content = ""
|
||||
@@ -174,6 +190,106 @@ Describe "Update-WinUtilSelections" {
|
||||
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
|
||||
@($script:sync.selectedAppx) | Should -Be @("WPFAppxExample")
|
||||
}
|
||||
|
||||
It "replaces selections only after every imported key is validated" {
|
||||
$script:sync.selectedApps.Add("WPFInstallExisting")
|
||||
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
|
||||
|
||||
Update-WinUtilSelections -flatJson @(
|
||||
"WPFInstallGit",
|
||||
"WPFFeatureSandbox"
|
||||
) -Replace
|
||||
|
||||
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
|
||||
@($script:sync.selectedTweaks) | Should -Be @()
|
||||
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
|
||||
}
|
||||
|
||||
It "preserves existing selections when an imported key is unsupported" {
|
||||
$script:sync.selectedApps.Add("WPFInstallExisting")
|
||||
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
|
||||
|
||||
{
|
||||
Update-WinUtilSelections -flatJson @(
|
||||
"WPFInstallGit",
|
||||
"NotAWinUtilKey"
|
||||
) -Replace
|
||||
} | Should -Throw "Unsupported selection key 'NotAWinUtilKey'."
|
||||
|
||||
@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
|
||||
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksExisting")
|
||||
@($script:sync.selectedFeatures) | Should -Be @()
|
||||
}
|
||||
|
||||
It "preserves existing selections when an imported key is not in the current catalog" {
|
||||
$script:sync.selectedApps.Add("WPFInstallExisting")
|
||||
|
||||
{
|
||||
Update-WinUtilSelections -flatJson @(
|
||||
"WPFInstallGit",
|
||||
"WPFInstallUnknown"
|
||||
) -Replace
|
||||
} | Should -Throw "Unknown selection key 'WPFInstallUnknown'."
|
||||
|
||||
@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
|
||||
@($script:sync.selectedTweaks) | Should -Be @()
|
||||
@($script:sync.selectedFeatures) | Should -Be @()
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-WPFImpex import selection state" {
|
||||
BeforeEach {
|
||||
New-WinUtilUiStateTestContext
|
||||
$script:sync.Form = [pscustomobject]@{}
|
||||
|
||||
Mock Reset-WPFCheckBoxes { }
|
||||
Mock Write-Error { }
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
|
||||
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It "replaces selections and resets the UI after a valid import" {
|
||||
$script:sync.selectedApps.Add("WPFInstallExisting")
|
||||
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
|
||||
$configPath = Join-Path $TestDrive "valid-config.json"
|
||||
@(
|
||||
"WPFInstallGit",
|
||||
"WPFFeatureSandbox"
|
||||
) | ConvertTo-Json | Set-Content -LiteralPath $configPath
|
||||
|
||||
Invoke-WPFImpex -type "import" -Config $configPath
|
||||
|
||||
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
|
||||
@($script:sync.selectedTweaks) | Should -Be @()
|
||||
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
|
||||
Should -Invoke -CommandName Reset-WPFCheckBoxes -Times 1 -Exactly -ParameterFilter {
|
||||
$doToggles -eq $true
|
||||
}
|
||||
Should -Invoke -CommandName Write-Error -Times 0 -Exactly
|
||||
}
|
||||
|
||||
It "preserves selections and does not reset the UI after an invalid import" {
|
||||
$script:sync.selectedApps.Add("WPFInstallExisting")
|
||||
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
|
||||
$configPath = Join-Path $TestDrive "invalid-config.json"
|
||||
@(
|
||||
"WPFInstallGit",
|
||||
"WPFInstallUnknown"
|
||||
) | ConvertTo-Json | Set-Content -LiteralPath $configPath
|
||||
|
||||
Invoke-WPFImpex -type "import" -Config $configPath
|
||||
|
||||
@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
|
||||
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksExisting")
|
||||
@($script:sync.selectedFeatures) | Should -Be @()
|
||||
Should -Invoke -CommandName Reset-WPFCheckBoxes -Times 0 -Exactly
|
||||
Should -Invoke -CommandName Write-Error -Times 1 -Exactly -ParameterFilter {
|
||||
$Message -like "An error occurred while importing: *Unknown selection key 'WPFInstallUnknown'.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-WPFSelectedCheckboxesUpdate" {
|
||||
|
||||
Reference in New Issue
Block a user