Files
IntuneManagement/UI/Avalonia/Extensions/IntuneManagerExportUI.ps1
T

209 lines
8.7 KiB
PowerShell

# Avalonia port of the Export dialog from UI/WPF/Extensions/IntuneManagerUI.ps1
# (Show-IntuneManagerExportForm). New file because Export is a self-contained
# subsystem and we don't want to grow the giant IntuneManagerUI files further
# (architecture rule R9).
#
# Same closure-free design rationale as IntuneManagerImportUI.ps1: form state
# lives in $script:_imExportFormState so click handlers can be plain
# scriptblocks (no .GetNewClosure() — that puts the SB into a dynamic module
# with no module-private function resolution).
function Show-IntuneManagerExportForm
{
if (-not $script:dgIntuneManagerObjects) { return }
$ui = $script:UIProvider
$exportForm = $ui.GetXamlObject((Join-Path $script:AppUIRootFolder 'XAML/ExportForm.axaml'))
if (-not $exportForm) { return }
$hostType = Get-AvaloniaHost
$txtPath = $hostType::FindByName($exportForm, 'txtExportPath')
$browse = $hostType::FindByName($exportForm, 'browseExportPath')
$chkObj = $hostType::FindByName($exportForm, 'chkAddObjectType')
$chkComp = $hostType::FindByName($exportForm, 'chkAddCompanyName')
$chkAssign = $hostType::FindByName($exportForm, 'chkExportAssignments')
$btnSel = $hostType::FindByName($exportForm, 'btnExportSelected')
$btnAll = $hostType::FindByName($exportForm, 'btnExportAll')
$btnCancel = $hostType::FindByName($exportForm, 'btnCancel')
$lblSel = $hostType::FindByName($exportForm, 'lblSelectedObject')
# Build the settings instance and let the data-side hooks add their own
# NoteProperties (per-type extra export options). These reach onto the
# CLR class via Add-Member so we can still read them after Save().
$exportSettings = [IntuneManagerExportSettings]::new()
$policyTypes = Get-IntuneManagerSelectedPolicyTypes
$policyTypes | Add-IntuneManagerExportProperties -ExportSettings $exportSettings
$policyTypes | Add-IntuneManagerExportUIExtensions -Form $exportForm
# Avalonia bindings would render against IntuneManagerExportSettings
# but the class has no INotifyPropertyChanged, so two-way round-trip
# silently fails. Push current values into controls and read them back
# imperatively on click.
if ($txtPath) { $txtPath.Text = [string]$exportSettings.ExportFolder }
if ($chkObj) { $chkObj.IsChecked = [bool]$exportSettings.AddObjectType }
if ($chkComp) { $chkComp.IsChecked = [bool]$exportSettings.AddCompanyName }
if ($chkAssign) { $chkAssign.IsChecked = [bool]$exportSettings.ExportAssignments }
# Selection summary label + Export Selected enable state mirror the WPF
# behaviour: prefer IsSelected-checked rows over the highlighted row.
# Scan the grid's CURRENT (filtered) ItemsSource - a row checked before the
# filter was typed must not be exported while invisible.
$checkedRows = @(@($script:dgIntuneManagerObjects.ItemsSource) | Where-Object { $_ -and $_.IsSelected })
$highlighted = $script:dgIntuneManagerObjects.SelectedItem
if ($lblSel) {
if ($checkedRows.Count -gt 0) {
$lblSel.Content = "$($checkedRows.Count) selected object(s)"
} elseif ($highlighted) {
$lblSel.Content = "Selected object: $($highlighted.Name)"
}
}
if ($btnSel) {
$btnSel.IsEnabled = ($checkedRows.Count -gt 0) -or ($null -ne $highlighted)
}
# Module-scope state container. Click handlers reference by name —
# no .GetNewClosure() so they survive Avalonia delegate dispatch.
$script:_imExportFormState = @{
Settings = $exportSettings
TxtPath = $txtPath
ChkObj = $chkObj
ChkComp = $chkComp
ChkAssign = $chkAssign
CheckedRows = $checkedRows
Highlighted = $highlighted
Action = $null
}
if ($browse) {
$browse.add_Click({
$st = $script:_imExportFormState
$ui2 = $script:UIProvider
if (-not $st) { return }
$folder = $ui2.ShowFolderPicker('Select root folder for export')
if ($folder) {
$st.Settings.ExportFolder = $folder
if ($st.TxtPath) { $st.TxtPath.Text = $folder }
}
})
}
if ($btnSel) {
$btnSel.add_Click({
$st = $script:_imExportFormState
if (-not $st) { return }
$st.Action = 'Selected'
# Keep the dialog open when the export bailed out on validation
# (e.g. no folder picked) - it used to close and discard the input.
$ranOk = $false
try { $ranOk = [bool](Invoke-IntuneManagerExportExecute) } catch { Write-LogError "Export execution failed" $_.Exception }
if (-not $ranOk) { return }
$script:UIProvider.CloseTopModalObject()
$script:_imExportFormState = $null
if ($script:dgIntuneManagerObjects) { $script:dgIntuneManagerObjects.Focus() }
})
}
if ($btnAll) {
$btnAll.add_Click({
$st = $script:_imExportFormState
if (-not $st) { return }
$st.Action = 'All'
$ranOk = $false
try { $ranOk = [bool](Invoke-IntuneManagerExportExecute) } catch { Write-LogError "Export execution failed" $_.Exception }
if (-not $ranOk) { return }
$script:UIProvider.CloseTopModalObject()
$script:_imExportFormState = $null
if ($script:dgIntuneManagerObjects) { $script:dgIntuneManagerObjects.Focus() }
})
}
if ($btnCancel) {
$btnCancel.add_Click({
$script:UIProvider.CloseTopModalObject()
$script:_imExportFormState = $null
if ($script:dgIntuneManagerObjects) { $script:dgIntuneManagerObjects.Focus() }
})
}
$exportForm.add_KeyDown({
param($s, $e)
if ($e.Key -eq [Avalonia.Input.Key]::Escape) {
$script:UIProvider.CloseTopModalObject()
$script:_imExportFormState = $null
$e.Handled = $true
}
})
# ShowModalForm is non-blocking; export runs inside the button handlers via
# Invoke-IntuneManagerExportExecute while the form state is live.
$ui.ShowModalForm("Export $($script:IntuneManagerSelectedObject.Title) objects", $exportForm, $true)
}
function Invoke-IntuneManagerExportExecute
{
# Runs the export for the live $script:_imExportFormState. Called from the
# Export button handlers (ShowModalForm does not block).
$st = $script:_imExportFormState
if (-not $st -or -not $st.Action -or $st.Action -eq 'Cancel') { return }
$ui = $script:UIProvider
$exportSettings = $st.Settings
$txtPath = $st.TxtPath
$chkObj = $st.ChkObj
$chkComp = $st.ChkComp
$chkAssign = $st.ChkAssign
$checkedRows = @($st.CheckedRows)
$highlighted = $st.Highlighted
# Pull form state back into the settings object before exporting.
if ($txtPath) { $exportSettings.ExportFolder = [string]$txtPath.Text }
if ($chkObj) { $exportSettings.AddObjectType = [bool]$chkObj.IsChecked }
if ($chkComp) { $exportSettings.AddCompanyName = [bool]$chkComp.IsChecked }
if ($chkAssign) { $exportSettings.ExportAssignments = [bool]$chkAssign.IsChecked }
if ([string]::IsNullOrWhiteSpace($exportSettings.ExportFolder)) {
$ui.ShowMessageBox("Pick an export root folder first.", "Export", "OK", "Error") | Out-Null
return $false
}
$rowsToExport = @()
if ($st.Action -eq 'Selected') {
if ($checkedRows.Count -gt 0) {
$rowsToExport = $checkedRows
} elseif ($highlighted) {
$rowsToExport = @($highlighted)
}
} else {
# Export All — use the current ItemsSource (filtered) to match what
# the user sees.
if ($script:dgIntuneManagerObjects.ItemsSource) {
$rowsToExport = @($script:dgIntuneManagerObjects.ItemsSource)
}
}
if ($rowsToExport.Count -eq 0) {
Write-Log "No rows to export" 2
return $false
}
Write-Status "Export $($script:IntuneManagerSelectedObject.Title)"
# Export-GraphPolicy expects IntunePolicyBase, not the CLR row wrapper —
# pull .Source off each row before piping.
$policiesToExport = @($rowsToExport | ForEach-Object { $_.Source } | Where-Object { $_ })
try {
$policiesToExport | Export-GraphPolicy -ExportSettings $exportSettings
} catch {
Write-LogError "Export-GraphPolicy failed" $_.Exception
$ui.ShowMessageBox("Export failed: $($_.Exception.Message)", "Error", "OK", "Error") | Out-Null
Write-Status ""
return $false
}
$exportSettings.Save()
Write-Status ""
if ($script:dgIntuneManagerObjects) { $script:dgIntuneManagerObjects.Focus() }
return $true
}