mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 19:05:38 +02:00
IntuneManagement 4.0.0-beta1
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
# Avalonia port of the Bulk Export dialog from
|
||||
# UI/WPF/Extensions/IntuneManagerUI.ps1 (Show-GraphBulkExportForm + helpers).
|
||||
# New file because Bulk Export is a self-contained subsystem and the WPF
|
||||
# original sits in the giant IntuneManagerUI file we're trying not to grow
|
||||
# further (architecture rule R9).
|
||||
|
||||
function Update-BulkExportObjectList
|
||||
{
|
||||
if (-not $script:dgBulkExportObjects) { return }
|
||||
|
||||
$rows = [System.Collections.Generic.List[BulkExportRowItem]]::new()
|
||||
|
||||
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) {
|
||||
$rows.Add([BulkExportRowItem]@{
|
||||
Title = [string]$intuneType.Title
|
||||
Selected = $true
|
||||
ObjectGroup = $null
|
||||
ObjectType = $intuneType
|
||||
})
|
||||
}
|
||||
} else {
|
||||
$sortedGroups = $script:bulkExportExportableGroups | Sort-Object Title
|
||||
foreach ($intuneGroup in $sortedGroups) {
|
||||
$defaultSelected = ?? $intuneGroup.BulkExport $true
|
||||
$rows.Add([BulkExportRowItem]@{
|
||||
Title = [string]$intuneGroup.Title
|
||||
Selected = [bool]$defaultSelected
|
||||
ObjectGroup = $intuneGroup
|
||||
ObjectType = $null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$script:bulkExportRows = @($rows)
|
||||
$script:dgBulkExportObjects.ItemsSource = $script:bulkExportRows
|
||||
}
|
||||
|
||||
function Get-BulkExportSelectedIds
|
||||
{
|
||||
if ($null -eq $script:bulkExportRows) { return @() }
|
||||
if ($script:bulkExportMode -eq "Type") {
|
||||
return @($script:bulkExportRows |
|
||||
Where-Object { $_.Selected -and $_.ObjectType -and $_.ObjectType.Id } |
|
||||
ForEach-Object { $_.ObjectType.Id })
|
||||
}
|
||||
return @($script:bulkExportRows |
|
||||
Where-Object { $_.Selected -and $_.ObjectGroup -and $_.ObjectGroup.Id } |
|
||||
ForEach-Object { $_.ObjectGroup.Id })
|
||||
}
|
||||
|
||||
function Show-GraphBulkExportForm
|
||||
{
|
||||
# CORRECTION (2026-08-25): the note that used to sit here had it exactly
|
||||
# backwards. Inside Avalonia handlers, $script: variables and bare
|
||||
# module-function lookups DO resolve - it is the function-LOCAL captures
|
||||
# that are stripped by ConvertTo-AvaloniaEventScriptBlock. Handler state
|
||||
# therefore lives in $script:_bulkExportFormState.
|
||||
$ui = $script:UIProvider
|
||||
|
||||
$form = $ui.GetXamlObject((Join-Path $script:AppUIRootFolder 'XAML/BulkExportForm.axaml'))
|
||||
if (-not $form) { return }
|
||||
|
||||
$hostType = Get-AvaloniaHost
|
||||
$script:dgBulkExportObjects = $hostType::FindByName($form, 'dgObjectsToExport')
|
||||
|
||||
$txtPath = $hostType::FindByName($form, 'txtExportPath')
|
||||
$btnBrowse = $hostType::FindByName($form, 'browseExportPath')
|
||||
$txtFilter = $hostType::FindByName($form, 'txtExportNameFilter')
|
||||
$chkAssign = $hostType::FindByName($form, 'chkExportAssignments')
|
||||
$chkCompany = $hostType::FindByName($form, 'chkAddCompanyName')
|
||||
$txtNested = $hostType::FindByName($form, 'txtNestedGroupLevels')
|
||||
$rbGroup = $hostType::FindByName($form, 'rbBulkExportViewGroup')
|
||||
$rbType = $hostType::FindByName($form, 'rbBulkExportViewType')
|
||||
$btnExport = $hostType::FindByName($form, 'btnExport')
|
||||
$btnClose = $hostType::FindByName($form, 'btnClose')
|
||||
|
||||
$exportSettings = [IntuneManagerExportSettings]::new()
|
||||
|
||||
# Handlers are re-bound to the module and lose function-local captures, so
|
||||
# every control they touch lives here instead (same pattern as Bulk
|
||||
# Compare). Without this the Export, Close, Escape and Browse handlers all
|
||||
# threw on their first line and the dialog was unusable.
|
||||
$script:_bulkExportFormState = [ordered]@{
|
||||
TxtPath = $null; TxtFilter = $null; ChkAssign = $null; ChkCompany = $null
|
||||
TxtNested = $null; HeaderCb = $null
|
||||
ExportSettings = $exportSettings
|
||||
}
|
||||
|
||||
# Register dynamic export-properties for every exportable policy type up
|
||||
# front 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 $form
|
||||
}
|
||||
|
||||
# Push current values into controls. IntuneManagerExportSettings has no
|
||||
# INotifyPropertyChanged so two-way binding would render but never
|
||||
# round-trip — read back imperatively on Export click.
|
||||
if ($txtPath) { $txtPath.Text = [string]$exportSettings.ExportFolder }
|
||||
if ($txtFilter) { $txtFilter.Text = [string]$exportSettings.Filter }
|
||||
if ($chkAssign) { $chkAssign.IsChecked = [bool]$exportSettings.ExportAssignments }
|
||||
if ($chkCompany) { $chkCompany.IsChecked = [bool]$exportSettings.AddCompanyName }
|
||||
if ($txtNested) { $txtNested.Text = [string]$exportSettings.ExportNestedGroupLevels }
|
||||
|
||||
$script:_bulkExportFormState.TxtPath = $txtPath
|
||||
$script:_bulkExportFormState.TxtFilter = $txtFilter
|
||||
$script:_bulkExportFormState.ChkAssign = $chkAssign
|
||||
$script:_bulkExportFormState.ChkCompany = $chkCompany
|
||||
$script:_bulkExportFormState.TxtNested = $txtNested
|
||||
|
||||
$script:bulkExportMode = "Group"
|
||||
Update-BulkExportObjectList
|
||||
|
||||
if ($btnBrowse) {
|
||||
$btnBrowse.add_Click((ConvertTo-AvaloniaEventScriptBlock {
|
||||
$st = $script:_bulkExportFormState
|
||||
if (-not $st) { return }
|
||||
$folder = $script:UIProvider.ShowFolderPicker(([string]$st.TxtPath.Text), 'Select root folder for export')
|
||||
if ($folder) {
|
||||
$st.ExportSettings.ExportFolder = $folder
|
||||
if ($st.TxtPath) { $st.TxtPath.Text = $folder }
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
# Select/deselect-all column header — initialised here so the per-mode
|
||||
# rebind below can simply flip it back on with $headerCb.IsChecked = $true.
|
||||
$headerCb = Initialize-AvaloniaGridSelectAllHeader -Grid $script:dgBulkExportObjects -BindingProperty 'Selected' -InitiallyChecked $true
|
||||
$script:_bulkExportFormState.HeaderCb = $headerCb
|
||||
|
||||
if ($rbGroup) {
|
||||
$rbGroup.add_IsCheckedChanged((ConvertTo-AvaloniaEventScriptBlock {
|
||||
param($s, $e)
|
||||
if (-not $s.IsChecked) { return }
|
||||
$script:bulkExportMode = "Group"
|
||||
Update-BulkExportObjectList
|
||||
$st = $script:_bulkExportFormState
|
||||
if ($st -and $st.HeaderCb) { $st.HeaderCb.IsChecked = $true }
|
||||
}))
|
||||
}
|
||||
if ($rbType) {
|
||||
$rbType.add_IsCheckedChanged((ConvertTo-AvaloniaEventScriptBlock {
|
||||
param($s, $e)
|
||||
if (-not $s.IsChecked) { return }
|
||||
$script:bulkExportMode = "Type"
|
||||
Update-BulkExportObjectList
|
||||
$st = $script:_bulkExportFormState
|
||||
if ($st -and $st.HeaderCb) { $st.HeaderCb.IsChecked = $true }
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
# 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).
|
||||
|
||||
if ($btnExport) {
|
||||
$btnExport.add_Click((ConvertTo-AvaloniaEventScriptBlock {
|
||||
$st = $script:_bulkExportFormState
|
||||
if (-not $st) { return }
|
||||
Set-BulkExportSettingsFromForm
|
||||
$exportSettings = $st.ExportSettings
|
||||
|
||||
$selection = Get-BulkExportSelectedIds
|
||||
$unit = if ($script:bulkExportMode -eq "Type") { "policy type" } else { "object group" }
|
||||
|
||||
if ($selection.Count -eq 0) {
|
||||
$ui.ShowMessageBox("Select at least one $unit to export.", "Bulk Export", "OK", "Warning") | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($exportSettings.ExportFolder)) {
|
||||
$ui.ShowMessageBox("Select an export folder.", "Bulk Export", "OK", "Warning") | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
try { $resolvedPath = [IO.Path]::GetFullPath($exportSettings.ExportFolder) }
|
||||
catch {
|
||||
$ui.ShowMessageBox("Invalid export folder path: $($_.Exception.Message)", "Bulk Export", "OK", "Error") | Out-Null
|
||||
return
|
||||
}
|
||||
$exportSettings.ExportFolder = $resolvedPath
|
||||
Write-Log "Bulk export starting. Folder: $resolvedPath"
|
||||
|
||||
$startParams = @{ ExportSettings = $exportSettings }
|
||||
if ($script:bulkExportMode -eq "Type") {
|
||||
$startParams.PolicyType = $selection
|
||||
} else {
|
||||
$startParams.PolicyGroup = $selection
|
||||
}
|
||||
|
||||
try {
|
||||
$summary = Start-GraphBulkExport @startParams
|
||||
Write-Status ""
|
||||
$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" }
|
||||
$ui.ShowMessageBox($msg, "Bulk Export", "OK", "Information") | Out-Null
|
||||
} catch {
|
||||
Write-LogError "Bulk export failed" $_.Exception
|
||||
Write-Status ""
|
||||
$ui.ShowMessageBox("Bulk export failed: $($_.Exception.Message)", "Bulk Export", "OK", "Error") | Out-Null
|
||||
}
|
||||
|
||||
$script:dgBulkExportObjects = $null
|
||||
$script:bulkExportRows = $null
|
||||
$script:_bulkExportFormState = $null
|
||||
Close-TopModalObject
|
||||
}))
|
||||
}
|
||||
|
||||
if ($btnClose) {
|
||||
$btnClose.add_Click((ConvertTo-AvaloniaEventScriptBlock {
|
||||
$script:dgBulkExportObjects = $null
|
||||
$script:bulkExportRows = $null
|
||||
$script:_bulkExportFormState = $null
|
||||
Close-TopModalObject
|
||||
}))
|
||||
}
|
||||
|
||||
$form.add_KeyDown((ConvertTo-AvaloniaEventScriptBlock {
|
||||
param($s, $e)
|
||||
if ($e.Key -eq [Avalonia.Input.Key]::Escape) {
|
||||
$script:dgBulkExportObjects = $null
|
||||
$script:bulkExportRows = $null
|
||||
$script:_bulkExportFormState = $null
|
||||
Close-TopModalObject
|
||||
$e.Handled = $true
|
||||
}
|
||||
}))
|
||||
|
||||
$ui.ShowModalForm("Bulk Export", $form, $true)
|
||||
}
|
||||
|
||||
function Set-BulkExportSettingsFromForm
|
||||
{
|
||||
# Pulls the live form values into the settings object. A module function
|
||||
# rather than a captured scriptblock: handlers cannot reach captures.
|
||||
$st = $script:_bulkExportFormState
|
||||
if (-not $st -or -not $st.ExportSettings) { return }
|
||||
$s = $st.ExportSettings
|
||||
|
||||
if ($st.TxtPath) {
|
||||
$raw = [string]$st.TxtPath.Text
|
||||
if ($raw) { $raw = $raw.Trim().Trim('"').Trim("'") }
|
||||
$s.ExportFolder = $raw
|
||||
}
|
||||
if ($st.TxtFilter) { $s.Filter = [string]$st.TxtFilter.Text }
|
||||
if ($st.ChkAssign) { $s.ExportAssignments = [bool]$st.ChkAssign.IsChecked }
|
||||
if ($st.ChkCompany) { $s.AddCompanyName = [bool]$st.ChkCompany.IsChecked }
|
||||
if ($st.TxtNested) {
|
||||
$parsed = 0
|
||||
if ([int]::TryParse([string]$st.TxtNested.Text, [ref]$parsed) -and $parsed -ge 1) {
|
||||
$s.ExportNestedGroupLevels = $parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user