Files
FFU/FFUDevelopment/FFUUI.Core/FFUUI.Core.Applications.psm1
T
rbalsleyMSFT 62b4816498 Improves application orchestration and BYO app handling
Enhances the orchestrator script to skip application installation steps if no corresponding app lists or files are present. This prevents unnecessary script executions and cleans up the output log.

Makes the 'Arguments' field optional when adding a 'Bring Your Own' application, simplifying the process for apps without arguments.

Ensures the 'Priority' field for applications is consistently saved as an integer to improve data integrity.
2025-07-17 16:50:38 -07:00

410 lines
17 KiB
PowerShell

# FFU UI Core Applications Module
# Contains UI-layer logic for the "Bring Your Own Apps" and related features.
# Function to update the enabled state of the Copy Apps button
function Update-CopyButtonState {
param(
[psobject]$State
)
$listView = $State.Controls.lstApplications
$copyButton = $State.Controls.btnCopyBYOApps
if ($listView -and $copyButton) {
$hasSource = $false
foreach ($item in $listView.Items) {
if ($null -ne $item -and $item.PSObject.Properties['Source'] -and -not [string]::IsNullOrWhiteSpace($item.Source)) {
$hasSource = $true
break
}
}
$copyButton.IsEnabled = $hasSource
}
}
# Function to remove application and reorder priorities
function Remove-Application {
param(
$priority,
[psobject]$State
)
$listView = $State.Controls.lstApplications
# Remove the item with the specified priority
$itemToRemove = $listView.Items | Where-Object { $_.Priority -eq $priority } | Select-Object -First 1
if ($itemToRemove) {
$listView.Items.Remove($itemToRemove)
# Reorder priorities for remaining items
Update-ListViewPriorities -ListView $listView
# Update the Copy Apps button state
Update-CopyButtonState -State $State
}
}
# Function to add a new BYO application from the UI
function Add-BYOApplication {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[psobject]$State
)
$name = $State.Controls.txtAppName.Text
$commandLine = $State.Controls.txtAppCommandLine.Text
$arguments = $State.Controls.txtAppArguments.Text
$source = $State.Controls.txtAppSource.Text
if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($commandLine)) {
[System.Windows.MessageBox]::Show("Please fill in all fields (Name and Command Line)", "Missing Information", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
return
}
$listView = $State.Controls.lstApplications
$priority = 1
if ($listView.Items.Count -gt 0) {
$priority = ($listView.Items | Measure-Object -Property Priority -Maximum).Maximum + 1
}
$application = [PSCustomObject]@{ Priority = $priority; Name = $name; CommandLine = $commandLine; Arguments = $arguments; Source = $source; CopyStatus = "" }
$listView.Items.Add($application)
$State.Controls.txtAppName.Text = ""
$State.Controls.txtAppCommandLine.Text = ""
$State.Controls.txtAppArguments.Text = ""
$State.Controls.txtAppSource.Text = ""
Update-CopyButtonState -State $State
}
# Function to add a new Apps Script Variable from the UI
function Add-AppsScriptVariable {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[psobject]$State
)
$key = $State.Controls.txtAppsScriptKey.Text.Trim()
$value = $State.Controls.txtAppsScriptValue.Text.Trim()
if ([string]::IsNullOrWhiteSpace($key)) {
[System.Windows.MessageBox]::Show("Apps Script Variable Key cannot be empty.", "Input Error", "OK", "Warning")
return
}
# Check for duplicate keys
$existingKey = $State.Controls.lstAppsScriptVariables.Items | Where-Object { $_.Key -eq $key }
if ($existingKey) {
[System.Windows.MessageBox]::Show("An Apps Script Variable with the key '$key' already exists.", "Duplicate Key", "OK", "Warning")
return
}
$newItem = [PSCustomObject]@{
IsSelected = $false # Add IsSelected property
Key = $key
Value = $value
}
$State.Data.appsScriptVariablesDataList.Add($newItem)
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
$State.Controls.txtAppsScriptKey.Clear()
$State.Controls.txtAppsScriptValue.Clear()
# Update the header checkbox state
if ($null -ne $State.Controls.chkSelectAllAppsScriptVariables) {
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstAppsScriptVariables -HeaderCheckBox $State.Controls.chkSelectAllAppsScriptVariables
}
}
# Function to remove selected Apps Script Variables from the list
function Remove-SelectedAppsScriptVariable {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[psobject]$State
)
$itemsToRemove = @($State.Data.appsScriptVariablesDataList | Where-Object { $_.IsSelected })
if ($itemsToRemove.Count -eq 0) {
[System.Windows.MessageBox]::Show("Please select one or more Apps Script Variables to remove.", "Selection Error", "OK", "Warning")
return
}
foreach ($itemToRemove in $itemsToRemove) {
$State.Data.appsScriptVariablesDataList.Remove($itemToRemove)
}
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
# Update the header checkbox state
if ($null -ne $State.Controls.chkSelectAllAppsScriptVariables) {
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstAppsScriptVariables -HeaderCheckBox $State.Controls.chkSelectAllAppsScriptVariables
}
}
# Function to save BYO applications to JSON
function Save-BYOApplicationList {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[psobject]$State
)
$listView = $State.Controls.lstApplications
if (-not $listView -or $listView.Items.Count -eq 0) {
[System.Windows.MessageBox]::Show("No applications to save.", "Save Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
return
}
try {
# Ensure items are sorted by current priority before saving
# Exclude CopyStatus when saving and ensure Priority is an integer
$applications = $listView.Items | Sort-Object Priority | Select-Object @{N = 'Priority'; E = { [int]$_.Priority } }, Name, CommandLine, Arguments, Source
$applications | ConvertTo-Json -Depth 5 | Set-Content -Path $Path -Force -Encoding UTF8
[System.Windows.MessageBox]::Show("Applications saved successfully to `"$Path`".", "Save Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
}
catch {
[System.Windows.MessageBox]::Show("Failed to save applications: $_", "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
}
}
# Function to load BYO applications from JSON
function Import-BYOApplicationList {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[psobject]$State
)
if (-not (Test-Path $Path)) {
[System.Windows.MessageBox]::Show("Application list file not found at `"$Path`".", "Import Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
return
}
try {
$applications = Get-Content -Path $Path -Raw | ConvertFrom-Json
$listView = $State.Controls.lstApplications
$listView.Items.Clear()
# Add items and sort by priority from the file
$sortedApps = $applications | Sort-Object Priority
foreach ($app in $sortedApps) {
# Ensure all properties exist, add CopyStatus
$appObject = [PSCustomObject]@{
Priority = $app.Priority # Keep original priority for now
Name = $app.Name
CommandLine = $app.CommandLine
Arguments = if ($app.PSObject.Properties['Arguments']) { $app.Arguments } else { "" } # Handle missing Arguments
Source = $app.Source
CopyStatus = "" # Initialize CopyStatus
}
$listView.Items.Add($appObject)
}
# Reorder priorities sequentially after loading
Update-ListViewPriorities -ListView $listView
# Update the Copy Apps button state
Update-CopyButtonState -State $State
[System.Windows.MessageBox]::Show("Applications imported successfully from `"$Path`".", "Import Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
}
catch {
[System.Windows.MessageBox]::Show("Failed to import applications: $_", "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
}
}
# Function to invoke the parallel copy process for BYO apps
function Invoke-CopyBYOApps {
param(
[psobject]$State,
[System.Windows.Controls.Button]$Button
)
$appsToCopy = $State.Controls.lstApplications.Items | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Source) }
if (-not $appsToCopy) {
[System.Windows.MessageBox]::Show("No applications with a source path specified.", "Copy BYO Apps", "OK", "Information")
return
}
$Button.IsEnabled = $false
$State.Controls.pbOverallProgress.Visibility = 'Visible'
$State.Controls.pbOverallProgress.Value = 0
$State.Controls.txtStatus.Text = "Starting BYO app copy..."
# Define necessary task-specific variables locally
$localAppsPath = $State.Controls.txtApplicationPath.Text
# Create hashtable for task-specific arguments
$taskArguments = @{
AppsPath = $localAppsPath
}
# Select only necessary properties before passing
$itemsToProcess = $appsToCopy | Select-Object Priority, Name, CommandLine, Arguments, Source
# Invoke the centralized parallel processing function
# Pass task type and task-specific arguments
Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess `
-ListViewControl $State.Controls.lstApplications `
-IdentifierProperty 'Name' `
-StatusProperty 'CopyStatus' `
-TaskType 'CopyBYO' `
-TaskArguments $taskArguments `
-CompletedStatusText "Copied" `
-ErrorStatusPrefix "Error: " `
-WindowObject $State.Window `
-MainThreadLogPath $State.LogFilePath
# Final status update (handled by Invoke-ParallelProcessing)
$State.Controls.pbOverallProgress.Visibility = 'Collapsed'
$Button.IsEnabled = $true
}
# Function to copy a single BYO application (Modified for ForEach-Object -Parallel)
function Start-CopyBYOApplicationTask {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[PSCustomObject]$ApplicationItemData, # Pass data, not the UI object
[Parameter(Mandatory)]
[string]$AppsPath, # Pass necessary path
[Parameter(Mandatory = $true)]
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue # Add queue parameter
# REMOVED: UI-related parameters
)
$priority = $ApplicationItemData.Priority
$appName = $ApplicationItemData.Name
$commandLine = $ApplicationItemData.CommandLine
$arguments = $ApplicationItemData.Arguments
$sourcePath = $ApplicationItemData.Source
$status = "Starting..." # Initial local status
$success = $false
# Initial status update
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
if ([string]::IsNullOrWhiteSpace($AppsPath)) {
$status = "Error: Apps Path not set"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
WriteLog "Copy error for $($appName): Apps Path not set."
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
}
if ([string]::IsNullOrWhiteSpace($sourcePath)) {
$status = "No source specified"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
# This isn't an error, just nothing to do. Consider it success.
$success = $true
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
}
if (-not (Test-Path -Path $sourcePath -PathType Container)) {
$status = "Error: Source path not found"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
WriteLog "Copy error for $($appName): Source path '$sourcePath' not found."
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
}
$win32BasePath = Join-Path -Path $AppsPath -ChildPath "Win32"
$destinationPath = Join-Path -Path $win32BasePath -ChildPath $appName
try {
# Check destination
if (Test-Path -Path $destinationPath -PathType Container) {
$folderSize = (Get-ChildItem -Path $destinationPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$status = "Already copied"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
WriteLog "Skipping copy for $($appName): Destination '$destinationPath' exists and has content."
$success = $true
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
}
else {
WriteLog "Destination '$destinationPath' exists but is empty/small. Proceeding with copy."
}
}
# Ensure base directory exists
if (-not (Test-Path -Path $win32BasePath -PathType Container)) {
New-Item -Path $win32BasePath -ItemType Directory -Force | Out-Null
WriteLog "Created directory: $win32BasePath"
}
# Perform the copy
$status = "Copying..."
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
WriteLog "Copying '$sourcePath' to '$destinationPath'..."
Copy-Item -Path $sourcePath -Destination $destinationPath -Recurse -Force -ErrorAction Stop
$status = "Copied successfully"
$success = $true
WriteLog "Successfully copied '$appName' to '$destinationPath'."
# ------------------------------------------------------------------
# Update (or create) UserAppList.json with the copied application
# ------------------------------------------------------------------
try {
WriteLog "Updating UserAppList.json for '$appName'..."
$userAppListPath = Join-Path -Path $AppsPath -ChildPath 'UserAppList.json'
# Build the new entry
$newEntry = [pscustomobject]@{
Priority = [int]$priority
Name = $appName
CommandLine = $commandLine
Arguments = $arguments
Source = $sourcePath
}
# Load existing list if present, ensuring it's always an array
if (Test-Path -Path $userAppListPath) {
try {
# Attempt to load and ensure it's an array
$appList = @(Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json -ErrorAction Stop)
}
catch {
WriteLog "Warning: Could not parse '$userAppListPath' or it's not a valid JSON array. Initializing as empty array. Error: $($_.Exception.Message)"
$appList = @() # Initialize as empty array on error
}
}
else {
$appList = @() # Initialize as empty array if file doesn't exist
}
# Ensure $appList is an array even if ConvertFrom-Json returned $null or a single object somehow
if ($null -eq $appList -or $appList -isnot [array]) {
# If it was a single object, wrap it in an array. Otherwise, start fresh.
$appList = if ($null -ne $appList) { @($appList) } else { @() }
}
# Skip adding if an entry with the same Name already exists
if (-not ($appList | Where-Object { $_.Name -eq $newEntry.Name })) {
# Now $appList is guaranteed to be an array, so += is safe
$appList += $newEntry
# Sort by Priority before saving
$sortedAppList = $appList | Sort-Object Priority
$sortedAppList | ConvertTo-Json -Depth 10 | Set-Content -Path $userAppListPath -Encoding UTF8
WriteLog "Added '$($newEntry.Name)' to '$userAppListPath'."
}
else {
WriteLog "'$appName' already exists in '$userAppListPath'."
}
}
catch {
WriteLog "Failed to update UserAppList.json for '$appName': $($_.Exception.Message)"
}
}
catch {
$errorMessage = $_.Exception.Message
$status = "Error: $($errorMessage)"
WriteLog "Copy error for $($appName): $($errorMessage)"
$success = $false
# Enqueue error status
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
}
# Enqueue final success status if applicable
if ($success) {
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
}
# Return the final status
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
}
Export-ModuleMember -Function *