Files
IntuneManagement/UI/WPF/Extensions/IntuneManagerBulkExportUIWPF.ps1
T

211 lines
9.3 KiB
PowerShell

# Bulk Export form.
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Show-GraphBulkExportForm
{
$script:bulkExportForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkExportForm.xaml"), $true)
if(-not $script:bulkExportForm) { return }
$script:dgObjectsToExport = $script:bulkExportForm.FindName("dgObjectsToExport")
$exportSettings = [IntuneManagerExportSettings]::new()
$script:bulkExportForm.DataContext = $exportSettings
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "browseExportPath", "add_click", {
$folder = Get-Folder ($script:UIProvider.GetXamlProperty($script:bulkExportForm, "txtExportPath", "Text")) "Select root folder for export"
if($folder)
{
$script:bulkExportForm.DataContext.ExportFolder = $folder
$tmp = $script:bulkExportForm.DataContext
$script:bulkExportForm.DataContext = $null
$script:bulkExportForm.DataContext = $tmp
}
})
# Register dynamic export-properties / UI extensions for every exportable
# policy type up front. Done once (not per mode switch) so toggling
# Group/API doesn't re-register and we never miss a type that only appears
# in the other view.
$script:bulkExportExportableGroups = @($script:IntuneGroups | Where-Object {
$_.Title -and (-not ($_.ShowButtons -is [Object[]]) -or $_.ShowButtons -contains "Export")
})
foreach($intuneGroup in $script:bulkExportExportableGroups)
{
$intuneGroup.PolicyTypes | Add-IntuneManagerExportProperties -ExportSettings $exportSettings
$intuneGroup.PolicyTypes | Add-IntuneManagerExportUIExtensions -Form $script:bulkExportForm
}
$column = Get-GridCheckboxColumn "Selected"
$script:dgObjectsToExport.Columns.Add($column)
$column.Header.IsChecked = $true # All items are checked by default
$column.Header.add_Click({
foreach($Item in $script:dgObjectsToExport.ItemsSource)
{
$Item.Selected = $this.IsChecked
}
$script:dgObjectsToExport.Items.Refresh()
}
)
# Add Object type column
$binding = [System.Windows.Data.Binding]::new("Title")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Object type"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:dgObjectsToExport.Columns.Add($column)
# Default view is "Group" (matches the IsChecked=True on the XAML radio).
$script:bulkExportMode = "Group"
Update-BulkExportObjectList
# Repopulate the grid when the user flips between Group and API views.
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "rbBulkExportViewGroup", "add_Checked", {
$script:bulkExportMode = "Group"
Update-BulkExportObjectList
})
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "rbBulkExportViewType", "add_Checked", {
$script:bulkExportMode = "Type"
Update-BulkExportObjectList
})
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "btnClose", "add_click", {
$script:bulkExportForm = $null
Show-ModalObject
})
# NOTE: the "Save settings for batch job" button was removed 2026-06-12 per
# user decision — scheduled/CI runs use the public drivers directly
# (Start-GraphBulkExport -SettingsFile files can still be produced with
# Save-GraphBulkExportSettings from a script).
# btnExport: collect the selected rows from the DataGrid and call the public
# driver. The UI does NOT contain export logic — it's a thin caller of
# Start-GraphBulkExport so the same operation is scriptable headlessly.
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "btnExport", "add_click", {
$selection = Get-BulkExportSelectedIds
$unit = if ($script:bulkExportMode -eq "Type") { "policy type" } else { "object group" }
if ($selection.Count -eq 0) {
$script:UIProvider.ShowMessageBox("Select at least one $unit to export.", "Bulk Export", "OK", "Warning") | Out-Null
return
}
# Read the path straight from the TextBox rather than relying on the binding —
# the field is bound TwoWay/PropertyChanged so they should agree, but pulling
# from the control directly is bullet-proof. Trim and unquote (users sometimes
# paste paths with surrounding quotes).
$rawPath = ($script:UIProvider.GetXamlProperty($script:bulkExportForm, "txtExportPath", "Text"))
if ($rawPath) { $rawPath = ([string]$rawPath).Trim().Trim('"').Trim("'") }
if (-not $rawPath) {
$script:UIProvider.ShowMessageBox("Select an export folder.", "Bulk Export", "OK", "Warning") | Out-Null
return
}
# Resolve to a full path so the success message shows the canonical location,
# not whatever relative-ish text the user typed.
try { $resolvedPath = [IO.Path]::GetFullPath($rawPath) }
catch {
$script:UIProvider.ShowMessageBox("Invalid export folder path: $($_.Exception.Message)", "Bulk Export", "OK", "Error") | Out-Null
return
}
$script:bulkExportForm.DataContext.ExportFolder = $resolvedPath
Write-Log "Bulk export starting. Folder: $resolvedPath"
$startParams = @{
ExportSettings = $script:bulkExportForm.DataContext
}
if ($script:bulkExportMode -eq "Type") {
$startParams.PolicyType = $selection
}
else {
$startParams.PolicyGroup = $selection
}
try {
$summary = Start-GraphBulkExport @startParams
Write-Status $null
$msg = ("Exported {0} policies across {1} types ({2} failed) in {3:hh\:mm\:ss}.`n`nFolder:`n{4}" -f `
$summary.Policies, $summary.Types, $summary.Failed, $summary.Duration, $resolvedPath)
$unknownNote = Get-IntuneUnknownSelectorSummary $summary.UnknownSelectors
if($unknownNote) { $msg += "`n`n$unknownNote" }
$script:UIProvider.ShowMessageBox($msg, "Bulk Export", "OK", "Information") | Out-Null
}
catch {
Write-LogError "Bulk export failed" $_.Exception
$script:UIProvider.ShowMessageBox("Bulk export failed: $($_.Exception.Message)", "Bulk Export", "OK", "Error") | Out-Null
}
$script:bulkExportForm = $null
Show-ModalObject
})
$script:UIProvider.ShowModalForm("Bulk Export", $script:bulkExportForm, $true)
}
# Rebuild the bulk-export DataGrid for the current $script:bulkExportMode.
# Group mode lists policy groups (Configuration, Compliance, ...); Type mode
# lists individual policy types (APIs). Both sort by Title so the order is
# stable instead of class-load order.
function Update-BulkExportObjectList
{
if (-not $script:dgObjectsToExport) { return }
$script:exportObjects = @()
if ($script:bulkExportMode -eq "Type") {
$exportableGroupIds = @($script:bulkExportExportableGroups | ForEach-Object { $_.Id })
$sortedTypes = $script:IntuneTypes |
Where-Object { $_.PolicyGroup -and ($exportableGroupIds -contains $_.PolicyGroup.Id) } |
Sort-Object Title
foreach ($intuneType in $sortedTypes) {
$script:exportObjects += New-Object PSObject -Property @{
Title = $intuneType.Title
Selected = $true
ObjectGroup = $null
ObjectType = $intuneType
}
}
}
else {
$sortedGroups = $script:bulkExportExportableGroups | Sort-Object Title
foreach ($intuneGroup in $sortedGroups) {
$script:exportObjects += New-Object PSObject -Property @{
Title = $intuneGroup.Title
Selected = (?? $intuneGroup.BulkExport $true)
ObjectGroup = $intuneGroup
ObjectType = $null
}
}
}
$script:dgObjectsToExport.ItemsSource = $script:exportObjects
}
# Pull the Id list out of the currently-checked rows. In Group mode that's
# PolicyGroup IDs (for -PolicyGroup); in Type mode it's PolicyType IDs (for
# -PolicyType). Returns @() when nothing is selected.
function Get-BulkExportSelectedIds
{
if ($script:bulkExportMode -eq "Type") {
return @($script:exportObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectType -and $_.ObjectType.Id } |
ForEach-Object { $_.ObjectType.Id })
}
return @($script:exportObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectGroup -and $_.ObjectGroup.Id } |
ForEach-Object { $_.ObjectGroup.Id })
}
# ─── Bulk Scope Tags ──────────────────────────────────────────────────────────
# Thin UI wrapper around Set-GraphBulkScopeTags. The form mirrors Bulk Export's
# Group / API selector for picking object types, adds an action selector
# (Add / Replace / Remove), an orphan-cleanup checkbox, and a dual-list scope
# tag picker. All business logic lives in the public command — the UI only
# collects inputs, calls it, and shows the summary.
# Get-BulkScopeTagCatalog moved to Internal/IntuneScopeTags.ps1 so the
# Avalonia tree can share it.