mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 19:05:38 +02:00
2444 lines
111 KiB
PowerShell
2444 lines
111 KiB
PowerShell
# UI/Avalonia counterpart of UI/WPF/Extensions/IntuneToolsUI.ps1 — minimum-viable
|
|
# port of the IntuneTools view so its left-nav items render and clicking them
|
|
# swaps placeholder content into grdToolsMain. The four real tool bodies
|
|
# (Show-IntuneAssignmentsTool, Show-IntuneFilterUsageTool, Show-ADMXImportTool,
|
|
# Show-ADMXRegValuesTool — ~2500 lines in the WPF file) are NOT ported yet;
|
|
# each is currently a placeholder TextBlock saying "not yet ported".
|
|
|
|
Add-AppEventHandler "AppInitialized" "Initialize-IntuneAssignmentsModule"
|
|
|
|
function Get-IntuneToolsViewItems
|
|
{
|
|
# Items must be CLR [ViewMenuItem] instances, not [PSCustomObject], so
|
|
# Avalonia's binder can resolve MenuLabel / IconImage on the lstMenuItems
|
|
# DataTemplate. See [[avalonia-binding-needs-clr-types]].
|
|
$items = @()
|
|
|
|
$items += [ViewMenuItem]@{
|
|
Id = 'IntuneAssignments'
|
|
Title = 'Intune Assignments'
|
|
MenuLabel = 'Intune Assignments'
|
|
Category = 'Assignments'
|
|
Description = 'List assignments for Intune objects, from an exported folder or directly from Intune.'
|
|
IconImage = Get-IntuneToolsItemIcon 'DeviceConfiguration'
|
|
}
|
|
|
|
$items += [ViewMenuItem]@{
|
|
Id = 'IntuneFilterUsage'
|
|
Title = 'Intune Filter Usage'
|
|
MenuLabel = 'Intune Filter Usage'
|
|
Category = 'Assignments'
|
|
Description = 'Show every assignment filter and the policies / apps that reference it.'
|
|
IconImage = Get-IntuneToolsItemIcon 'DeviceConfiguration'
|
|
}
|
|
|
|
$items += [ViewMenuItem]@{
|
|
Id = 'ADMXImport'
|
|
Title = 'ADMX Import'
|
|
MenuLabel = 'ADMX Import'
|
|
Category = 'ADMX'
|
|
Description = 'Load an .admx (+ .adml) file, edit policy settings, and ingest as a Custom OMA-URI device-configuration profile.'
|
|
IconImage = Get-IntuneToolsItemIcon 'DeviceConfiguration'
|
|
}
|
|
|
|
$items += [ViewMenuItem]@{
|
|
Id = 'ADMXRegValues'
|
|
Title = 'Reg Values'
|
|
MenuLabel = 'Reg Values'
|
|
Category = 'ADMX'
|
|
Description = 'Build a Custom OMA-URI policy from manually-added registry keys/values (HKLM + HKCU).'
|
|
IconImage = Get-IntuneToolsItemIcon 'DeviceConfiguration'
|
|
}
|
|
|
|
return $items
|
|
}
|
|
|
|
function Get-IntuneToolsItemIcon
|
|
{
|
|
param([string]$IconName)
|
|
|
|
$ui = $script:UIProvider
|
|
# Icons live under UI/Avalonia/XAML/Icons/*.axaml (Avalonia-flavoured
|
|
# ports of the WPF UI/WPF/XAML/Icons/*.xaml shape XAML, generated by
|
|
# XAML/Icons/_Convert-Icons.ps1). Each is a Viewbox/Canvas/Path tree the
|
|
# lstMenuItems ContentControl can host directly.
|
|
if (-not $IconName) { return $null }
|
|
$iconFile = Join-Path $script:AppUIRootFolder "XAML/Icons/$IconName.axaml"
|
|
if (-not [IO.File]::Exists($iconFile)) { return $null }
|
|
return ($ui.GetXamlObject($iconFile))
|
|
}
|
|
|
|
function Get-IntuneToolsViewPanel
|
|
{
|
|
$ui = $script:UIProvider
|
|
$panel = $ui.GetXamlObject((Join-Path $script:AppUIRootFolder 'XAML/IntuneTools.axaml'))
|
|
if (-not $panel) { return $null }
|
|
|
|
$script:grdToolsMain = (Get-AvaloniaHost)::FindByName($panel, 'grdToolsMain')
|
|
return $panel
|
|
}
|
|
|
|
function Invoke-IntuneToolsActivateItem
|
|
{
|
|
param($SelectedItem)
|
|
|
|
if (-not $SelectedItem -or -not $script:grdToolsMain) { return $null }
|
|
|
|
# WPF returned a CollectionViewGroup sentinel for group headers; the
|
|
# Avalonia flat-list path doesn't surface those (yet).
|
|
if (-not $SelectedItem.Id) {
|
|
Write-Log "Unable to activate Intune tool. Selected item did not include a tool Id." 2
|
|
return $null
|
|
}
|
|
|
|
Write-Log "Activate tool: $($SelectedItem.Title)"
|
|
|
|
switch ($SelectedItem.Id)
|
|
{
|
|
'IntuneAssignments' { Show-IntuneAssignmentsTool }
|
|
'IntuneFilterUsage' { Show-IntuneFilterUsageTool }
|
|
'ADMXImport' { Show-ADMXImportTool }
|
|
'ADMXRegValues' { Show-ADMXRegValuesTool }
|
|
default
|
|
{
|
|
Write-Log "Unknown tool selected: $($SelectedItem.Id)" 2
|
|
$script:grdToolsMain.Children.Clear()
|
|
}
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
# Placeholder tool bodies — each clears grdToolsMain and writes a "not yet
|
|
# ported" notice. Real bodies are large (DataGrids, ADMX editors, filter
|
|
# resolution) and live in UI/WPF/Extensions/IntuneToolsUI.ps1; port them
|
|
# incrementally when their flows are needed in Avalonia.
|
|
|
|
function Set-IntuneToolPlaceholder
|
|
{
|
|
param([string]$ToolName)
|
|
|
|
if (-not $script:grdToolsMain) { return }
|
|
$script:grdToolsMain.Children.Clear()
|
|
|
|
$tb = [Avalonia.Controls.TextBlock]::new()
|
|
$tb.Text = "$ToolName - not yet ported to Avalonia."
|
|
$tb.HorizontalAlignment = [Avalonia.Layout.HorizontalAlignment]::Center
|
|
$tb.VerticalAlignment = [Avalonia.Layout.VerticalAlignment]::Center
|
|
$tb.FontStyle = [Avalonia.Media.FontStyle]::Italic
|
|
$tb.Foreground = [Avalonia.Media.Brushes]::Gray
|
|
$script:grdToolsMain.Children.Add($tb) | Out-Null
|
|
}
|
|
|
|
# ─── Intune Assignments tool ──────────────────────────────────────────────────
|
|
#
|
|
# Avalonia port of UI/WPF/Extensions/IntuneToolsUI.ps1 Show-IntuneAssignmentsTool.
|
|
# Notable differences vs WPF:
|
|
# - PSCustomObject rows from Get-IntuneAssignmentRow are converted to
|
|
# [IntuneAssignmentRowItem] CLR instances before the DataGrid sees them
|
|
# (Avalonia's binder can't read PSObject NoteProperty).
|
|
# - Provider ComboBox uses ItemTemplate {Binding Name} instead of WPF
|
|
# DisplayMemberPath; SelectedItem.Value substitutes for SelectedValuePath.
|
|
# - Filter rebuilds an ItemsSource subset rather than using WPF's
|
|
# ICollectionView.Items.Filter (Avalonia DataGrid doesn't expose that).
|
|
# - Folder-options panel doesn't use DataContext binding (provider classes
|
|
# don't implement INPC); Register-IntuneAssignmentsProviderEvents sets
|
|
# TextBox.Text imperatively and wires TextChanged + Browse to push back.
|
|
# - Save button goes through (Get-AvaloniaHost)::SaveFilePicker instead of
|
|
# WinForms SaveFileDialog (R13 — WindowsAPICodePack lives only in UI/).
|
|
|
|
function Show-IntuneAssignmentsTool
|
|
{
|
|
$ui = $script:UIProvider
|
|
if (-not $script:grdToolsMain) { return }
|
|
$script:grdToolsMain.Children.Clear()
|
|
|
|
$panel = $ui.GetXamlObject((Join-Path $script:AppUIRootFolder 'XAML/IntuneToolsAssignments.axaml'))
|
|
if (-not $panel) {
|
|
Write-Log "Failed to load IntuneToolsAssignments.axaml" 3
|
|
Set-IntuneToolPlaceholder 'Intune Assignments'
|
|
return
|
|
}
|
|
|
|
$script:intuneAssignmentsPanel = $panel
|
|
$script:intuneAssignmentsProviderPanelCache = @{}
|
|
# Keep last-loaded rows across panel rebuilds so the user doesn't have to
|
|
# re-run the load every time they navigate away from and back to the tool.
|
|
if (-not $script:intuneAssignmentsRows) { $script:intuneAssignmentsRows = @() }
|
|
|
|
$script:cbIntuneAssignmentsProvider = (Get-AvaloniaHost)::FindByName($panel, 'cbIntuneAssignmentsProvider')
|
|
$script:ccIntuneAssignmentsProviderOptions = (Get-AvaloniaHost)::FindByName($panel, 'ccIntuneAssignmentsProviderOptions')
|
|
$script:dgIntuneAssignments = (Get-AvaloniaHost)::FindByName($panel, 'dgIntuneAssignments')
|
|
$script:txtIntuneAssignmentsFilter = (Get-AvaloniaHost)::FindByName($panel, 'txtIntuneAssignmentsFilter')
|
|
$script:txtIntuneAssignmentsCount = (Get-AvaloniaHost)::FindByName($panel, 'txtIntuneAssignmentsCount')
|
|
$script:cbIntuneAssignmentsView = (Get-AvaloniaHost)::FindByName($panel, 'cbIntuneAssignmentsView')
|
|
|
|
if ($script:cbIntuneAssignmentsProvider) {
|
|
$script:cbIntuneAssignmentsProvider.ItemsSource = @($script:intuneAssignmentsProviders)
|
|
|
|
# DisplayMemberPath -> ItemTemplate binding (Avalonia ComboBox has no
|
|
# DisplayMemberPath property).
|
|
|
|
# Restore previously-selected provider. SelectedValuePath doesn't exist
|
|
# in Avalonia ComboBox; match by .Value on the source list.
|
|
$savedValue = Get-SettingStoreValue 'IntuneAssignments' 'Provider' 'folder'
|
|
$selected = $script:intuneAssignmentsProviders | Where-Object { $_.Value -eq $savedValue } | Select-Object -First 1
|
|
if (-not $selected -and $script:intuneAssignmentsProviders.Count -gt 0) {
|
|
$selected = $script:intuneAssignmentsProviders[0]
|
|
}
|
|
if ($selected) { $script:cbIntuneAssignmentsProvider.SelectedItem = $selected }
|
|
|
|
# Run the options-panel switch synchronously once before wiring the
|
|
# event handler, so the initial Set fires exactly once.
|
|
Set-IntuneAssignmentsProviderOptions $script:cbIntuneAssignmentsProvider
|
|
|
|
$script:cbIntuneAssignmentsProvider.add_SelectionChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
Set-IntuneAssignmentsProviderOptions $S
|
|
}))
|
|
}
|
|
|
|
$ui.AddXamlEvent($panel, 'btnGetIntuneAssignments', 'Add_Click', ({
|
|
Start-IntuneAssignmentsLoad
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'btnIntuneAssignmentsCopy', 'Add_Click', ({
|
|
$items = @(Get-IntuneToolsDataGridVisibleItems $script:dgIntuneAssignments)
|
|
if ($items.Count -eq 0) { return }
|
|
($items | Select-Object Name, Type, AssignmentCount, HasFilters, IncludedString, ExcludedString, IncludedFilterString, ExcludedFilterString | ConvertTo-Csv -NoTypeInformation) | Set-Clipboard
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'btnIntuneAssignmentsSave', 'Add_Click', ({
|
|
$items = @(Get-IntuneToolsDataGridVisibleItems $script:dgIntuneAssignments)
|
|
if ($items.Count -eq 0) { return }
|
|
|
|
$initialName = "IntuneAssignments_$((Get-Date).ToString('yyyyMMdd-HHmm'))"
|
|
# Reopen at the folder used last (WPF sets InitialDirectory); the
|
|
# setting was written on every save but never read back.
|
|
$lastDir = Get-SettingStoreValue 'IntuneAssignments' 'LastSaveDirectory'
|
|
if (-not $lastDir) { $lastDir = Get-SettingValue 'RootFolder' }
|
|
$picked = (Get-AvaloniaHost)::SaveFilePicker(
|
|
$script:Window,
|
|
'Save assignments',
|
|
$initialName,
|
|
'csv',
|
|
'CSV files',
|
|
'*.csv',
|
|
[string]$lastDir)
|
|
|
|
if ($picked) {
|
|
Save-SettingStoreValue 'IntuneAssignments' 'LastSaveDirectory' ([IO.FileInfo]$picked).DirectoryName
|
|
($items | Select-Object Name, Type, AssignmentCount, HasFilters, IncludedString, ExcludedString, IncludedFilterString, ExcludedFilterString | ConvertTo-Csv -NoTypeInformation) | Out-File -LiteralPath $picked -Force -Encoding UTF8
|
|
}
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'txtIntuneAssignmentsFilter', 'Add_TextChanged', ({
|
|
Update-IntuneAssignmentsFilter
|
|
}))
|
|
|
|
if ($script:cbIntuneAssignmentsView) {
|
|
# Wrap in the CLR combo type - a PSCustomObject would bind blank here.
|
|
$viewItems = [System.Collections.Generic.List[NameValueComboItem]]::new()
|
|
foreach ($option in (Get-IntuneAssignmentViewOptions)) {
|
|
[void]$viewItems.Add([NameValueComboItem]@{ Name = $option.Name; Value = $option.Value; Source = $option })
|
|
}
|
|
$script:cbIntuneAssignmentsView.ItemsSource = $viewItems
|
|
# First option (Show all) is the default.
|
|
$script:cbIntuneAssignmentsView.SelectedIndex = 0
|
|
|
|
$script:cbIntuneAssignmentsView.add_SelectionChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
Update-IntuneAssignmentsFilter
|
|
}))
|
|
}
|
|
|
|
if (@($script:intuneAssignmentsRows).Count -gt 0 -and $script:dgIntuneAssignments) {
|
|
$script:dgIntuneAssignments.ItemsSource = $script:intuneAssignmentsRows
|
|
Update-IntuneAssignmentsFilter
|
|
}
|
|
|
|
$script:grdToolsMain.Children.Add($panel) | Out-Null
|
|
}
|
|
|
|
function Set-IntuneAssignmentsProviderOptions
|
|
{
|
|
param($Control)
|
|
|
|
$ui = $script:UIProvider
|
|
if (-not $Control) { return }
|
|
$Provider = $Control.SelectedItem
|
|
if (-not $Provider) { return }
|
|
|
|
Save-SettingStoreValue 'IntuneAssignments' 'Provider' $Provider.Value
|
|
|
|
$providerPanel = $null
|
|
if ($Provider.OptionsXaml) {
|
|
if ($script:intuneAssignmentsProviderPanelCache.ContainsKey($Provider.Value)) {
|
|
$providerPanel = $script:intuneAssignmentsProviderPanelCache[$Provider.Value]
|
|
} else {
|
|
$panelPath = Join-Path $script:AppUIRootFolder "XAML/$($Provider.OptionsXaml).axaml"
|
|
if (Test-Path -LiteralPath $panelPath) {
|
|
$providerPanel = $ui.GetXamlObject($panelPath)
|
|
if ($providerPanel) {
|
|
$script:intuneAssignmentsProviderPanelCache[$Provider.Value] = $providerPanel
|
|
Register-IntuneAssignmentsProviderEvents $providerPanel $Provider
|
|
}
|
|
} else {
|
|
Write-Log "Provider options panel not found: $panelPath" 3
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($script:ccIntuneAssignmentsProviderOptions) {
|
|
$script:ccIntuneAssignmentsProviderOptions.Content = $providerPanel
|
|
$script:ccIntuneAssignmentsProviderOptions.IsVisible = [bool]$providerPanel
|
|
}
|
|
}
|
|
|
|
function Register-IntuneAssignmentsProviderEvents
|
|
{
|
|
param($Panel, $Provider)
|
|
|
|
$ui = $script:UIProvider
|
|
if ($Provider -is [IntuneAssignmentsFolderProvider]) {
|
|
$Provider.ExportPath = Get-SettingStoreValue 'IntuneAssignments' 'ExportPath'
|
|
|
|
$txtPath = (Get-AvaloniaHost)::FindByName($Panel, 'txtIntuneAssignmentsExportPath')
|
|
if ($txtPath) {
|
|
$txtPath.Text = [string]$Provider.ExportPath
|
|
# Two-way sync: typing pushes into Provider.ExportPath. No INPC needed.
|
|
$txtPath.Tag = $Provider
|
|
$txtPath.add_TextChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
if ($S.Tag) { $S.Tag.ExportPath = [string]$S.Text }
|
|
}))
|
|
}
|
|
|
|
$browse = (Get-AvaloniaHost)::FindByName($Panel, 'browseIntuneAssignmentsExportPath')
|
|
if ($browse -and $txtPath) {
|
|
$browse.Tag = @{ Provider = $Provider; TextBox = $txtPath }
|
|
$browse.add_Click((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
$tag = $S.Tag
|
|
# $script:UIProvider by name - a captured function-local $ui is
|
|
# stripped by ConvertTo-AvaloniaEventScriptBlock (closure law).
|
|
# Start the picker at the current path like WPF does.
|
|
$folder = $script:UIProvider.ShowFolderPicker([string]$tag.Provider.ExportPath, 'Select root folder for exported objects')
|
|
if ($folder) {
|
|
$tag.Provider.ExportPath = $folder
|
|
$tag.TextBox.Text = $folder # TextChanged will refresh Provider again — harmless
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
function Start-IntuneAssignmentsLoad
|
|
{
|
|
$ui = $script:UIProvider
|
|
if (-not $script:cbIntuneAssignmentsProvider) { return }
|
|
|
|
$provider = $script:cbIntuneAssignmentsProvider.SelectedItem
|
|
if (-not $provider) {
|
|
$ui.ShowMessageBox('Select an input source first', 'Intune Assignments', 'OK', 'Error')
|
|
return
|
|
}
|
|
|
|
try {
|
|
if (-not $provider.Validate()) { return }
|
|
|
|
$provider.SaveSettings()
|
|
|
|
Write-Status 'Get Intune assignments'
|
|
|
|
$script:dgIntuneAssignments.ItemsSource = $null
|
|
$rawRows = @($provider.GetAssignments())
|
|
|
|
# Coerce each PSCustomObject row into a CLR class instance so Avalonia
|
|
# bindings (Name / Type / HasFilters / ...) resolve.
|
|
$rows = @($rawRows | ForEach-Object {
|
|
if (-not $_) { return }
|
|
[IntuneAssignmentRowItem]@{
|
|
Name = [string]$_.Name
|
|
Type = [string]$_.Type
|
|
AssignmentCount = [int]$_.AssignmentCount
|
|
HasFilters = [bool]$_.HasFilters
|
|
IncludedString = [string]$_.IncludedString
|
|
ExcludedString = [string]$_.ExcludedString
|
|
IncludedFilterString = [string]$_.IncludedFilterString
|
|
ExcludedFilterString = [string]$_.ExcludedFilterString
|
|
Id = [string]$_.Id
|
|
Object = $_.Object
|
|
Included = @($_.Included)
|
|
Excluded = @($_.Excluded)
|
|
IncludedFilters = @($_.IncludedFilters)
|
|
ExcludedFilters = @($_.ExcludedFilters)
|
|
}
|
|
})
|
|
|
|
$script:intuneAssignmentsRows = $rows
|
|
Update-IntuneAssignmentsFilter # sets ItemsSource + summary
|
|
} catch {
|
|
$ui.ShowMessageBox($_.Exception.Message, 'Intune Assignments', 'OK', 'Error')
|
|
} finally {
|
|
Write-Status ''
|
|
}
|
|
}
|
|
|
|
function Get-IntuneAssignmentsSelectedView
|
|
{
|
|
# Value of the View combo, defaulting to All before the panel is built.
|
|
if (-not $script:cbIntuneAssignmentsView) { return 'All' }
|
|
$value = "$($script:cbIntuneAssignmentsView.SelectedItem.Value)"
|
|
if ([string]::IsNullOrEmpty($value)) { return 'All' }
|
|
return $value
|
|
}
|
|
|
|
function Update-IntuneAssignmentsFilter
|
|
{
|
|
if (-not $script:dgIntuneAssignments) { return }
|
|
if (-not $script:txtIntuneAssignmentsFilter) { return }
|
|
|
|
$text = "$($script:txtIntuneAssignmentsFilter.Text)".Trim()
|
|
$view = Get-IntuneAssignmentsSelectedView
|
|
|
|
$script:dgIntuneAssignments.ItemsSource = $null
|
|
|
|
if ([string]::IsNullOrEmpty($text) -and $view -eq 'All') {
|
|
$script:dgIntuneAssignments.ItemsSource = $script:intuneAssignmentsRows
|
|
} else {
|
|
$pattern = [regex]::Escape($text)
|
|
# Where-Object pipes items as PSObject wrappers; Avalonia's DataGrid
|
|
# text-column binder reflects on the runtime type and silently fails
|
|
# against the wrapper. Collect into a typed List so rows stay as
|
|
# bare IntuneAssignmentRowItem instances.
|
|
$filtered = [System.Collections.Generic.List[IntuneAssignmentRowItem]]::new()
|
|
foreach ($row in $script:intuneAssignmentsRows) {
|
|
if (-not (Test-IntuneAssignmentViewMatch $row $view)) { continue }
|
|
if ([string]::IsNullOrEmpty($text)) { [void]$filtered.Add($row); continue }
|
|
if (
|
|
($row.Name -and $row.Name -match $pattern) -or
|
|
($row.Type -and $row.Type -match $pattern) -or
|
|
($row.IncludedString -and $row.IncludedString -match $pattern) -or
|
|
($row.ExcludedString -and $row.ExcludedString -match $pattern) -or
|
|
($row.IncludedFilterString -and $row.IncludedFilterString -match $pattern) -or
|
|
($row.ExcludedFilterString -and $row.ExcludedFilterString -match $pattern) -or
|
|
($row.HasFilters -and 'Has filters' -match $pattern)
|
|
) {
|
|
[void]$filtered.Add($row)
|
|
}
|
|
}
|
|
$script:dgIntuneAssignments.ItemsSource = $filtered
|
|
}
|
|
|
|
Update-IntuneAssignmentsSummary
|
|
}
|
|
|
|
function Update-IntuneAssignmentsSummary
|
|
{
|
|
if (-not $script:txtIntuneAssignmentsCount) { return }
|
|
$total = @($script:intuneAssignmentsRows).Count
|
|
$visible = @($script:dgIntuneAssignments.ItemsSource).Count
|
|
$script:txtIntuneAssignmentsCount.Text =
|
|
if ($total -eq $visible) { "$total objects" } else { "$visible of $total objects" }
|
|
}
|
|
|
|
function Get-IntuneToolsDataGridVisibleItems
|
|
{
|
|
param($DataGrid)
|
|
if (-not $DataGrid -or -not $DataGrid.ItemsSource) { return @() }
|
|
return @($DataGrid.ItemsSource)
|
|
}
|
|
|
|
# ─── Intune Filter Usage tool ────────────────────────────────────────────────
|
|
#
|
|
# Avalonia port of UI/WPF/Extensions/IntuneToolsUI.ps1 Show-IntuneFilterUsageTool.
|
|
# Same translation pattern as Intune Assignments — DataGrid items converted
|
|
# from PSCustomObject to [IntuneFilterUsageRowItem], filter via ItemsSource
|
|
# rebuild, Save via Avalonia SaveFilePicker.
|
|
#
|
|
# Data layer (Get-IntuneFilterUsageData + Get-IntuneFilterPayloadDispatch +
|
|
# Get-IntuneEnrollmentConfiguration*) is duplicated verbatim from WPF. The
|
|
# functions don't touch any WPF API — they're pure Graph data layer that
|
|
# cleanup.md (R2) flags as misplaced in a UI file. Move to Internal/ in a
|
|
# future cleanup and both UI trees can share one copy.
|
|
|
|
function Show-IntuneFilterUsageTool
|
|
{
|
|
$ui = $script:UIProvider
|
|
if (-not $script:grdToolsMain) { return }
|
|
$script:grdToolsMain.Children.Clear()
|
|
|
|
$panel = $ui.GetXamlObject((Join-Path $script:AppUIRootFolder 'XAML/IntuneToolsFilterUsage.axaml'))
|
|
if (-not $panel) {
|
|
Write-Log "Failed to load IntuneToolsFilterUsage.axaml" 3
|
|
Set-IntuneToolPlaceholder 'Intune Filter Usage'
|
|
return
|
|
}
|
|
|
|
$script:intuneFilterUsagePanel = $panel
|
|
if (-not $script:intuneFilterUsageRows) { $script:intuneFilterUsageRows = @() }
|
|
|
|
$script:dgIntuneFilterUsage = (Get-AvaloniaHost)::FindByName($panel, 'dgIntuneFilterUsage')
|
|
$script:txtIntuneFilterUsageFilter = (Get-AvaloniaHost)::FindByName($panel, 'txtIntuneFilterUsageFilter')
|
|
$script:txtIntuneFilterUsageCount = (Get-AvaloniaHost)::FindByName($panel, 'txtIntuneFilterUsageCount')
|
|
$script:btnGetIntuneFilterUsage = (Get-AvaloniaHost)::FindByName($panel, 'btnGetIntuneFilterUsage')
|
|
|
|
if (-not $script:dgIntuneFilterUsage -or -not $script:btnGetIntuneFilterUsage) {
|
|
Write-Log "Intune Filter Usage XAML loaded, but required controls were not found" 3
|
|
return
|
|
}
|
|
|
|
$ui.AddXamlEvent($panel, 'btnGetIntuneFilterUsage', 'Add_Click', ({
|
|
Start-IntuneFilterUsageLoad
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'btnIntuneFilterUsageCopy', 'Add_Click', ({
|
|
$items = @(Get-IntuneToolsDataGridVisibleItems $script:dgIntuneFilterUsage)
|
|
if ($items.Count -eq 0) { return }
|
|
($items | Select-Object FilterName, PolicyName, PayloadType, Mode, GroupName | ConvertTo-Csv -NoTypeInformation) | Set-Clipboard
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'btnIntuneFilterUsageSave', 'Add_Click', ({
|
|
$items = @(Get-IntuneToolsDataGridVisibleItems $script:dgIntuneFilterUsage)
|
|
if ($items.Count -eq 0) { return }
|
|
|
|
$initialName = "IntuneFilterUsage_$((Get-Date).ToString('yyyyMMdd-HHmm'))"
|
|
# Reopen at the folder used last (WPF sets InitialDirectory); the
|
|
# setting was written on every save but never read back.
|
|
$lastDir = Get-SettingStoreValue 'IntuneFilterUsage' 'LastSaveDirectory'
|
|
if (-not $lastDir) { $lastDir = Get-SettingValue 'RootFolder' }
|
|
$picked = (Get-AvaloniaHost)::SaveFilePicker(
|
|
$script:Window,
|
|
'Save filter usage',
|
|
$initialName,
|
|
'csv',
|
|
'CSV files',
|
|
'*.csv',
|
|
[string]$lastDir)
|
|
|
|
if ($picked) {
|
|
Save-SettingStoreValue 'IntuneFilterUsage' 'LastSaveDirectory' ([IO.FileInfo]$picked).DirectoryName
|
|
($items | Select-Object FilterName, PolicyName, PayloadType, Mode, GroupName | ConvertTo-Csv -NoTypeInformation) | Out-File -LiteralPath $picked -Force -Encoding UTF8
|
|
}
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'txtIntuneFilterUsageFilter', 'Add_TextChanged', ({
|
|
Update-IntuneFilterUsageFilter
|
|
}))
|
|
|
|
if (@($script:intuneFilterUsageRows).Count -gt 0 -and $script:dgIntuneFilterUsage) {
|
|
$script:dgIntuneFilterUsage.ItemsSource = $script:intuneFilterUsageRows
|
|
Update-IntuneFilterUsageFilter
|
|
}
|
|
|
|
$script:grdToolsMain.Children.Add($panel) | Out-Null
|
|
}
|
|
|
|
function Start-IntuneFilterUsageLoad
|
|
{
|
|
$ui = $script:UIProvider
|
|
if (-not $script:dgIntuneFilterUsage) { return }
|
|
|
|
if ($script:btnGetIntuneFilterUsage) { $script:btnGetIntuneFilterUsage.IsEnabled = $false }
|
|
|
|
try {
|
|
# Reset per-load lazy caches so a re-run picks up fresh server state.
|
|
$script:_intuneEnrollmentConfigCache = $null
|
|
$script:_intuneManagedAppPolicyCache = $null
|
|
|
|
Write-Status 'Get Intune filter usage'
|
|
|
|
$script:dgIntuneFilterUsage.ItemsSource = $null
|
|
try {
|
|
$rawRows = @(Get-IntuneFilterUsageData)
|
|
} catch {
|
|
Write-LogError 'Intune Filter Usage: load failed' $_.Exception
|
|
Write-Status ''
|
|
$ui.ShowMessageBox("Failed to load filter usage:`n`n$($_.Exception.Message)", 'Intune Filter Usage', 'OK', 'Error')
|
|
return
|
|
}
|
|
|
|
# PSCustomObject -> CLR class for Avalonia binding.
|
|
$rows = @($rawRows | ForEach-Object {
|
|
if (-not $_) { return }
|
|
[IntuneFilterUsageRowItem]@{
|
|
FilterName = [string]$_.FilterName
|
|
Platform = [string]$_.Platform
|
|
FilterType = [string]$_.FilterType
|
|
PolicyName = [string]$_.PolicyName
|
|
PayloadType = [string]$_.PayloadType
|
|
Mode = [string]$_.Mode
|
|
GroupId = [string]$_.GroupId
|
|
GroupName = [string]$_.GroupName
|
|
}
|
|
})
|
|
|
|
$script:intuneFilterUsageRows = $rows
|
|
Update-IntuneFilterUsageFilter # sets ItemsSource + summary
|
|
|
|
Write-Status ''
|
|
} finally {
|
|
if ($script:btnGetIntuneFilterUsage) { $script:btnGetIntuneFilterUsage.IsEnabled = $true }
|
|
}
|
|
}
|
|
|
|
function Update-IntuneFilterUsageFilter
|
|
{
|
|
if (-not $script:dgIntuneFilterUsage) { return }
|
|
if (-not $script:txtIntuneFilterUsageFilter) { return }
|
|
|
|
$text = "$($script:txtIntuneFilterUsageFilter.Text)".Trim()
|
|
|
|
$script:dgIntuneFilterUsage.ItemsSource = $null
|
|
|
|
if ([string]::IsNullOrEmpty($text)) {
|
|
$script:dgIntuneFilterUsage.ItemsSource = $script:intuneFilterUsageRows
|
|
} else {
|
|
$pattern = [regex]::Escape($text)
|
|
# Same wrapping issue as Update-IntuneAssignmentsFilter — collect
|
|
# into a typed List so DataGrid text columns reflect the row class.
|
|
$filtered = [System.Collections.Generic.List[IntuneFilterUsageRowItem]]::new()
|
|
foreach ($row in $script:intuneFilterUsageRows) {
|
|
if (
|
|
($row.FilterName -and $row.FilterName -match $pattern) -or
|
|
($row.PolicyName -and $row.PolicyName -match $pattern) -or
|
|
($row.PayloadType -and $row.PayloadType -match $pattern) -or
|
|
($row.Mode -and $row.Mode -match $pattern) -or
|
|
($row.GroupName -and $row.GroupName -match $pattern)
|
|
) {
|
|
[void]$filtered.Add($row)
|
|
}
|
|
}
|
|
$script:dgIntuneFilterUsage.ItemsSource = $filtered
|
|
}
|
|
|
|
Update-IntuneFilterUsageSummary
|
|
}
|
|
|
|
function Update-IntuneFilterUsageSummary
|
|
{
|
|
if (-not $script:txtIntuneFilterUsageCount) { return }
|
|
$total = @($script:intuneFilterUsageRows).Count
|
|
$visible = @($script:dgIntuneFilterUsage.ItemsSource).Count
|
|
$script:txtIntuneFilterUsageCount.Text =
|
|
if ($total -eq $visible) { "$total rows" } else { "$visible of $total rows" }
|
|
}
|
|
|
|
# ─── Filter-usage data layer (verbatim copy from WPF; see cleanup.md R2) ─────
|
|
|
|
# ─── ADMX Import tool — Slices 5a1+5a2+5a3+5a4 wired ─────────────────────────
|
|
#
|
|
# Avalonia port of UI/WPF/Extensions/IntuneToolsUI.ps1 Show-ADMXImportTool.
|
|
# Loads IntuneToolsADMX.axaml, caches every named control the WPF version
|
|
# uses, and wires every handler (Load ADMX / Load ADML / Import + Import-tab
|
|
# TextBox / CheckBox / Add-Random + DataGrid DoubleTap / context-menu Edit).
|
|
#
|
|
# Differences from WPF:
|
|
# - WinForms OpenFileDialog -> (Get-AvaloniaHost)::OpenFilePicker (R13: this
|
|
# project doesn't carry a WinForms / WindowsAPICodePack dependency in
|
|
# UI/Avalonia/).
|
|
# - The picker returns a string (or $null on cancel) rather than a DialogResult
|
|
# + FileName pair, so the click handlers branch on truthiness.
|
|
|
|
function Show-ADMXImportTool
|
|
{
|
|
$ui = $script:UIProvider
|
|
if (-not $script:grdToolsMain) { return }
|
|
$script:grdToolsMain.Children.Clear()
|
|
|
|
$panel = $ui.GetXamlObject((Join-Path $script:AppUIRootFolder 'XAML/IntuneToolsADMX.axaml'))
|
|
if (-not $panel) {
|
|
Write-Log "Failed to load IntuneToolsADMX.axaml" 3
|
|
Set-IntuneToolPlaceholder 'ADMX Import'
|
|
return
|
|
}
|
|
|
|
# Cache panel + controls used by event handlers. FindByName once at panel-init
|
|
# so click/select handlers don't re-probe the visual tree.
|
|
$script:admxImportPanel = $panel
|
|
$script:btnADMXLoadADMX = (Get-AvaloniaHost)::FindByName($panel, 'btnADMXLoadADMX')
|
|
$script:btnADMXLoadADML = (Get-AvaloniaHost)::FindByName($panel, 'btnADMXLoadADML')
|
|
$script:btnADMXImport = (Get-AvaloniaHost)::FindByName($panel, 'btnADMXImport')
|
|
$script:btnADMXPolicyNameRandom = (Get-AvaloniaHost)::FindByName($panel, 'btnADMXPolicyNameRandom')
|
|
$script:tvADMXCategories = (Get-AvaloniaHost)::FindByName($panel, 'tvADMXCategories')
|
|
$script:dgADMXCategoryPolicies = (Get-AvaloniaHost)::FindByName($panel, 'dgADMXCategoryPolicies')
|
|
|
|
# The ADMX rows are PSCustomObjects, and Avalonia's binder cannot read
|
|
# NoteProperties ({Binding Name} renders empty cells - see
|
|
# [[avalonia-binding-needs-clr-types]]). Rebind each column through
|
|
# IntunePolicyPathConverter with a self-binding (empty path = the row),
|
|
# the same trick the main grid uses for ObjectColumns overrides.
|
|
if ($script:dgADMXCategoryPolicies) {
|
|
$admxPathConverter = (Get-AvaloniaHost)::CreatePathConverter()
|
|
$admxColPaths = @('Name', 'SettingStatusText', 'Category')
|
|
for ($i = 0; $i -lt [Math]::Min($admxColPaths.Count, $script:dgADMXCategoryPolicies.Columns.Count); $i++) {
|
|
$b = New-Object Avalonia.Data.Binding ''
|
|
$b.Mode = [Avalonia.Data.BindingMode]::OneWay
|
|
$b.Converter = $admxPathConverter
|
|
$b.ConverterParameter = $admxColPaths[$i]
|
|
$script:dgADMXCategoryPolicies.Columns[$i].Binding = $b
|
|
}
|
|
}
|
|
$script:txtADMXProfileName = (Get-AvaloniaHost)::FindByName($panel, 'txtADMXProfileName')
|
|
$script:txtADMXProfileDescription = (Get-AvaloniaHost)::FindByName($panel, 'txtADMXProfileDescription')
|
|
$script:txtADMXPolicyFileName = (Get-AvaloniaHost)::FindByName($panel, 'txtADMXPolicyFileName')
|
|
$script:txtADMXPolicyAppName = (Get-AvaloniaHost)::FindByName($panel, 'txtADMXPolicyAppName')
|
|
$script:txtADMXPolicyIngestName = (Get-AvaloniaHost)::FindByName($panel, 'txtADMXPolicyIngestName')
|
|
$script:chkADMXPolicyIngest = (Get-AvaloniaHost)::FindByName($panel, 'chkADMXPolicyIngest')
|
|
|
|
# Per-tool state — initialised here so file-dialog handlers can write to it
|
|
# without a null-check inside the click body.
|
|
$script:currentADMXFile = $null
|
|
|
|
# Idempotency for tree/grid event registration. Show-ADMXImportTool can be
|
|
# called multiple times across nav switches; the cached panel persists in
|
|
# memory for the lifetime of the IntuneTools view, so events shouldn't
|
|
# double-register.
|
|
$script:_admxImportEventsRegistered = $false
|
|
|
|
$ui.AddXamlEvent($panel, 'btnADMXLoadADMX', 'add_Click', ({
|
|
$startPath = Get-SettingStoreValue 'Tools' 'ADMXLastADMXFile'
|
|
$picked = (Get-AvaloniaHost)::OpenFilePicker(
|
|
$script:Window,
|
|
'Select ADMX file',
|
|
$startPath,
|
|
'ADMX files',
|
|
'*.admx')
|
|
if ($picked) {
|
|
$script:currentADMXFile = [IO.FileInfo]$picked
|
|
Save-SettingStoreValue 'Tools' 'ADMXLastADMXFile' $picked
|
|
Write-Status "Loading policy settings from $($script:currentADMXFile.Name)"
|
|
Start-ADMXLoadFile $picked
|
|
Write-Status ''
|
|
}
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'btnADMXLoadADML', 'add_Click', ({
|
|
$startPath = Get-SettingStoreValue 'Tools' 'ADMXLastADMLFile'
|
|
$picked = (Get-AvaloniaHost)::OpenFilePicker(
|
|
$script:Window,
|
|
'Select ADML file',
|
|
$startPath,
|
|
'ADML files',
|
|
'*.adml')
|
|
if ($picked) {
|
|
Save-SettingStoreValue 'Tools' 'ADMXLastADMLFile' $picked
|
|
Write-Status "Loading ADML policy $picked"
|
|
Invoke-ADMXLoadSettings $picked
|
|
Write-Status ''
|
|
}
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'btnADMXImport', 'add_Click', ({
|
|
Write-Status 'Import ADMX policy'
|
|
Import-ADMXPolicyToIntune
|
|
Write-Status ''
|
|
}))
|
|
|
|
# Append-or-replace GUID suffix on the policy file name. The trailing
|
|
# segment after "_" is the unique-id portion; if it's already a 32-char
|
|
# GUID we leave the existing prefix alone and replace only the suffix.
|
|
$ui.AddXamlEvent($panel, 'btnADMXPolicyNameRandom', 'add_Click', ({
|
|
$guid = [Guid]::NewGuid().Guid
|
|
if ($script:txtADMXPolicyFileName.Text) {
|
|
$parts = $script:txtADMXPolicyFileName.Text.Split('_')
|
|
if ($parts[-1].Length -eq $guid.Length) {
|
|
$script:txtADMXPolicyFileName.Text = (($parts[0..($parts.Count - 2)] -join '_') + '_' + $guid)
|
|
}
|
|
else {
|
|
$script:txtADMXPolicyFileName.Text = ($script:txtADMXPolicyFileName.Text + '_' + $guid)
|
|
}
|
|
}
|
|
else {
|
|
$script:txtADMXPolicyFileName.Text = $guid
|
|
}
|
|
}))
|
|
|
|
$script:grdToolsMain.Children.Add($panel) | Out-Null
|
|
}
|
|
|
|
# ─── ADMX Import — helpers (data layer, pure functions) ───────────────────────
|
|
#
|
|
# Slice 5a2 — ports the ADMX/ADML parser + category-tree builder. Pure functions
|
|
# read $script:_admx* state set up by Start-ADMXLoadFile / Invoke-ADMXLoadSettings;
|
|
# none of them touch UI controls (the TreeViewItem renderer is the lone
|
|
# exception and is clearly named).
|
|
#
|
|
# Differences from WPF (UI/WPF/Extensions/IntuneToolsUI.ps1):
|
|
# - Add-ADMXCategoryTreeNodeRecursive instantiates [Avalonia.Controls.TreeViewItem]
|
|
# instead of [System.Windows.Controls.TreeViewItem]; the API surface used
|
|
# (Header, Tag, Items.Add, Add-Member NoteProperty AllPolicies) carries over.
|
|
|
|
function Add-ADMXCategoriesRecursive
|
|
{
|
|
param($Categories, $Parent, $SettingClass)
|
|
|
|
foreach ($cat in $Categories.ref) {
|
|
$catPath = Get-ADMXCategoryIdPath $cat
|
|
$tvObj = $Parent
|
|
foreach ($catName in $catPath.Split('/')) {
|
|
$curParent = $tvObj.Children | Where-Object { $_.CategoryNode.name -eq $catName }
|
|
if (-not $curParent) {
|
|
$catNode = $script:_admxXML.policyDefinitions.categories.SelectSingleNode("$($script:_admxNSPrefix)category[@name='$catName']", $script:_admxNS)
|
|
$curParent = [PSCustomObject]@{
|
|
Name = Get-ADMXADMLString $catNode
|
|
CategoryName = $catName
|
|
CategoryNode = $catNode
|
|
SettingClass = $SettingClass
|
|
Children = @()
|
|
}
|
|
$tvObj.Children += $curParent
|
|
}
|
|
$tvObj = $curParent
|
|
}
|
|
}
|
|
}
|
|
|
|
function Add-ADMXCategoryTreeNodeRecursive
|
|
{
|
|
param($Obj, $Parent)
|
|
|
|
$tvItem = [Avalonia.Controls.TreeViewItem]::new()
|
|
$tvItem.Header = $Obj.Name
|
|
$tvItem.Tag = $Obj
|
|
[void]$Parent.Items.Add($tvItem)
|
|
|
|
$Obj.Children | Sort-Object -Property Name | ForEach-Object {
|
|
Add-ADMXCategoryTreeNodeRecursive $_ $tvItem
|
|
}
|
|
}
|
|
|
|
# ─── ADMX Import — loaders ────────────────────────────────────────────────────
|
|
|
|
function Start-ADMXLoadFile
|
|
{
|
|
param($FileName)
|
|
|
|
$ui = $script:UIProvider
|
|
# Hard reset — partial state from a previous file would cross-contaminate
|
|
# the tree-building below.
|
|
$script:_admxNS = $null
|
|
$script:_admxNSPrefix = $null
|
|
$script:_admxXML = $null
|
|
$script:_admxPolicies = @()
|
|
$script:_admxPoliciesHT = @{}
|
|
$script:_admxSupportedOn = @()
|
|
$script:_admxCategoryPaths = @{}
|
|
$script:_admxStringTable = @{}
|
|
$script:_admxlngADML = $null
|
|
|
|
if ($script:txtADMXProfileName) { $script:txtADMXProfileName.Text = '' }
|
|
if ($script:txtADMXProfileDescription) { $script:txtADMXProfileDescription.Text = '' }
|
|
if ($script:txtADMXPolicyFileName) { $script:txtADMXPolicyFileName.Text = '' }
|
|
if ($script:txtADMXPolicyIngestName) { $script:txtADMXPolicyIngestName.Text = '' }
|
|
if ($script:txtADMXPolicyAppName) { $script:txtADMXPolicyAppName.Text = '' }
|
|
if ($script:btnADMXLoadADML) { $script:btnADMXLoadADML.IsEnabled = $false }
|
|
|
|
$admxFI = [IO.FileInfo]$FileName
|
|
if (-not $admxFI.Exists) {
|
|
$ui.ShowMessageBox("ADMX file not found: $FileName", 'ADMX Import', 'OK', 'Error')
|
|
return
|
|
}
|
|
|
|
# Convention: paired .adml lives in en-US\ next to the .admx, or sometimes
|
|
# in the same folder. Missing ADML is a soft warning — Get-ADMXADMLString
|
|
# falls back to raw attributes if so.
|
|
$admlFile = [IO.Path]::Combine($admxFI.DirectoryName, "en-US", "$($admxFI.BaseName).adml")
|
|
if (-not [IO.File]::Exists($admlFile)) {
|
|
$admlFile = [IO.Path]::Combine($admxFI.DirectoryName, "$($admxFI.BaseName).adml")
|
|
}
|
|
if (-not [IO.File]::Exists($admlFile)) {
|
|
Write-Log "Could not find an ADML file alongside $FileName" 2
|
|
$admlFile = $null
|
|
}
|
|
|
|
try {
|
|
Write-Log "Load ADMX file $FileName"
|
|
[xml]$script:_admxXML = [IO.File]::ReadAllText($FileName)
|
|
|
|
$namespace = $script:_admxXML.DocumentElement.NamespaceURI
|
|
if ($namespace) {
|
|
$script:_admxNS = New-Object System.Xml.XmlNamespaceManager($script:_admxXML.NameTable)
|
|
$script:_admxNS.AddNamespace('ns', $namespace)
|
|
$script:_admxNSPrefix = 'ns:'
|
|
}
|
|
else {
|
|
$script:_admxNS = $null
|
|
$script:_admxNSPrefix = ''
|
|
}
|
|
|
|
# Default the App Id from the ADMX namespace prefix; fall back to the
|
|
# filename if that's missing.
|
|
$prefix = $script:_admxXML.policyDefinitions.policyNamespaces.SelectSingleNode("$($script:_admxNSPrefix)target[@prefix]", $script:_admxNS)
|
|
$policyId = $null
|
|
if ($prefix) {
|
|
if ($prefix.namespace) { $policyId = $prefix.namespace.Split('.')[-1] }
|
|
else { $policyId = $prefix.prefix }
|
|
}
|
|
if (-not $policyId) {
|
|
$policyId = $admxFI.BaseName -replace ' ', ''
|
|
Write-Log 'Failed to get policy id from ADMX namespace; using file base name' 2
|
|
}
|
|
if ($script:txtADMXPolicyAppName) { $script:txtADMXPolicyAppName.Text = $policyId }
|
|
if ($script:txtADMXPolicyFileName) { $script:txtADMXPolicyFileName.Text = $policyId }
|
|
}
|
|
catch {
|
|
Write-LogError 'Failed to load ADMX file' $_.Exception
|
|
return
|
|
}
|
|
|
|
if ($script:btnADMXLoadADML) { $script:btnADMXLoadADML.IsEnabled = $true }
|
|
Invoke-ADMXLoadSettings $admlFile
|
|
}
|
|
|
|
function Invoke-ADMXLoadSettings
|
|
{
|
|
param($AdmlFile)
|
|
|
|
if ($AdmlFile -and [IO.File]::Exists($AdmlFile)) {
|
|
try {
|
|
Write-Log "Load ADML file $AdmlFile"
|
|
[xml]$script:_admxlngADML = [IO.File]::ReadAllText($AdmlFile)
|
|
}
|
|
catch {
|
|
Write-LogError "Failed to load ADML file $AdmlFile" $_.Exception
|
|
}
|
|
}
|
|
|
|
$script:_admxStringTable = @{}
|
|
if ($script:_admxlngADML) {
|
|
foreach ($strNode in $script:_admxlngADML.policyDefinitionResources.resources.stringTable.string) {
|
|
if (-not $script:_admxStringTable.ContainsKey($strNode.id)) {
|
|
$script:_admxStringTable.Add($strNode.id, $strNode.'#text')
|
|
}
|
|
}
|
|
}
|
|
|
|
# supportedOn table — augmented with Windows.adml's SUPPORTED_* entries so
|
|
# policies that reference common Windows constants still resolve in the
|
|
# SettingProperties dialog (slice 5a3).
|
|
$script:_admxSupportedOn = @()
|
|
foreach ($polObj in $script:_admxXML.policyDefinitions.supportedOn.definitions.definition) {
|
|
$script:_admxSupportedOn += [PSCustomObject]@{
|
|
Id = $polObj.Name
|
|
DisplayName = (Get-ADMXADMLString $polObj)
|
|
}
|
|
}
|
|
if ($script:_admxWindowsADML) {
|
|
foreach ($winString in ($script:_admxWindowsADML.policyDefinitionResources.resources.stringTable.string | Where-Object Id -like 'SUPPORTED_*')) {
|
|
$script:_admxSupportedOn += [PSCustomObject]@{
|
|
Id = $winString.id
|
|
DisplayName = $winString.'#text'
|
|
}
|
|
}
|
|
}
|
|
|
|
$devicePolicies = @()
|
|
$userPolicies = @()
|
|
|
|
foreach ($polObj in $script:_admxXML.policyDefinitions.policies.policy) {
|
|
$category = if ($polObj.parentCategory.ref) { Get-ADMXCategoryNamePath $polObj.parentCategory.ref '/' } else { $null }
|
|
$displayName = Get-ADMXADMLString $polObj
|
|
$description = Get-ADMXADMLString $polObj 'explainText'
|
|
|
|
# Class can be Both / Machine / User. "Both" expands into two virtual
|
|
# entries so it shows up in both Device + User configuration trees.
|
|
$classArr = switch ($polObj.Class) {
|
|
'Both' { @('Device','User') }
|
|
'Machine' { @('Device') }
|
|
default { @($polObj.Class) }
|
|
}
|
|
|
|
foreach ($class in $classArr) {
|
|
$key = "$($polObj.Name)_$class"
|
|
if ($script:_admxPoliciesHT.ContainsKey($key)) { continue }
|
|
|
|
$newSetting = [PSCustomObject]@{
|
|
Name = $displayName
|
|
Description = $description
|
|
OMAURIName = $null
|
|
OMAURIDescription = $null
|
|
Category = $category
|
|
CategoryId = $polObj.parentCategory.ref
|
|
Id = $polObj.Name
|
|
Definition = $polObj
|
|
SettingStatus = $null
|
|
SettingStatusText = $null
|
|
PolicySettings = $null
|
|
PolicyDefinition = $null
|
|
ElementsPanel = $null
|
|
ManualConfig = $false
|
|
SettingClass = $class
|
|
SupportedOn = $null
|
|
}
|
|
$script:_admxPoliciesHT.Add($key, $newSetting)
|
|
$script:_admxPolicies += $newSetting
|
|
if ($class -eq 'User') { $userPolicies += $newSetting }
|
|
else { $devicePolicies += $newSetting }
|
|
}
|
|
}
|
|
|
|
$script:_admxPolicies | ForEach-Object { Set-ADMXSettingStatusText $_ }
|
|
$script:_admxPolicies = $script:_admxPolicies | Sort-Object -Property Name
|
|
|
|
if ($script:tvADMXCategories) { $script:tvADMXCategories.Items.Clear() }
|
|
|
|
$treeItems = @()
|
|
|
|
# Device Configuration root
|
|
$tvItem = [PSCustomObject]@{ Name = 'Computer Configuration'; Children = @() }
|
|
if ($script:_admxXML.policyDefinitions.policies) {
|
|
$policies = $script:_admxXML.policyDefinitions.policies.SelectNodes("$($script:_admxNSPrefix)policy[@class = 'Both' or @class = 'Machine']", $script:_admxNS)
|
|
if ($policies) {
|
|
$categories = $policies.parentCategory | Select-Object ref -Unique
|
|
Add-ADMXCategoriesRecursive $categories $tvItem 'Device'
|
|
}
|
|
}
|
|
$treeItems += $tvItem
|
|
|
|
# User Configuration root
|
|
$tvItem = [PSCustomObject]@{ Name = 'User Configuration'; Children = @() }
|
|
if ($script:_admxXML.policyDefinitions.policies) {
|
|
$policies = $script:_admxXML.policyDefinitions.policies.SelectNodes("$($script:_admxNSPrefix)policy[@class = 'Both' or @class = 'User']", $script:_admxNS)
|
|
if ($policies) {
|
|
$categories = $policies.parentCategory | Select-Object ref -Unique
|
|
Add-ADMXCategoriesRecursive $categories $tvItem 'User'
|
|
}
|
|
}
|
|
$treeItems += $tvItem
|
|
|
|
$treeItems | ForEach-Object { Add-ADMXCategoryTreeNodeRecursive $_ $script:tvADMXCategories }
|
|
|
|
# "All Policies" leaf under each root — quick way to see everything.
|
|
if ($devicePolicies.Count -gt 0 -and $script:tvADMXCategories.Items.Count -ge 1) {
|
|
$tv = [Avalonia.Controls.TreeViewItem]::new()
|
|
$tv.Header = 'All Policies'
|
|
$tv.Tag = $devicePolicies
|
|
$tv | Add-Member -MemberType NoteProperty -Name 'AllPolicies' -Value $true
|
|
[void]$script:tvADMXCategories.Items[0].Items.Add($tv)
|
|
}
|
|
if ($userPolicies.Count -gt 0 -and $script:tvADMXCategories.Items.Count -ge 2) {
|
|
$tv = [Avalonia.Controls.TreeViewItem]::new()
|
|
$tv.Header = 'All Policies'
|
|
$tv.Tag = $userPolicies
|
|
$tv | Add-Member -MemberType NoteProperty -Name 'AllPolicies' -Value $true
|
|
try { [void]$script:tvADMXCategories.Items[1].Items.Add($tv) } catch { }
|
|
}
|
|
|
|
Register-ADMXImportTreeEvents
|
|
}
|
|
|
|
function Register-ADMXImportTreeEvents
|
|
{
|
|
# Idempotent — only registers once per panel lifetime. ScriptBlocks read
|
|
# $script: state, so they work the same across reloads of ADMX files.
|
|
if ($script:_admxImportEventsRegistered) { return }
|
|
$script:_admxImportEventsRegistered = $true
|
|
|
|
if ($script:tvADMXCategories) {
|
|
# WPF SelectedItemChanged -> Avalonia SelectionChanged on TreeView
|
|
# (TreeView extends SelectingItemsControl; raises SelectionChanged when
|
|
# the selected node changes).
|
|
$script:tvADMXCategories.add_SelectionChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
$sel = $script:tvADMXCategories.SelectedItem
|
|
if (-not $sel) { return }
|
|
|
|
# When Items were populated with TreeViewItem instances directly,
|
|
# SelectedItem is the TreeViewItem itself; .Tag carries our model.
|
|
$hasAllPolicies = ($sel.PSObject.Properties['AllPolicies']) -and $sel.AllPolicies -eq $true
|
|
|
|
if ($hasAllPolicies) {
|
|
$script:dgADMXCategoryPolicies.Columns[0].Width = [Avalonia.Controls.DataGridLength]::Auto
|
|
$script:dgADMXCategoryPolicies.Columns[1].Width = [Avalonia.Controls.DataGridLength]::Auto
|
|
$script:dgADMXCategoryPolicies.Columns[2].Width = [Avalonia.Controls.DataGridLength]::Auto
|
|
$script:dgADMXCategoryPolicies.Columns[2].IsVisible = $true
|
|
$list = $sel.Tag
|
|
}
|
|
else {
|
|
$script:dgADMXCategoryPolicies.Columns[0].Width = [Avalonia.Controls.DataGridLength]::new(1, [Avalonia.Controls.DataGridLengthUnitType]::Star)
|
|
$script:dgADMXCategoryPolicies.Columns[1].Width = [Avalonia.Controls.DataGridLength]::Auto
|
|
$script:dgADMXCategoryPolicies.Columns[2].IsVisible = $false
|
|
$list = @($script:_admxPolicies | Where-Object {
|
|
$_.CategoryId -eq $sel.Tag.CategoryName -and
|
|
$_.SettingClass -eq $sel.Tag.SettingClass
|
|
})
|
|
}
|
|
$script:dgADMXCategoryPolicies.ItemsSource = [System.Collections.ObjectModel.ObservableCollection[object]]::new(@($list))
|
|
}))
|
|
}
|
|
|
|
if ($script:dgADMXCategoryPolicies) {
|
|
# WPF MouseDoubleClick -> Avalonia DoubleTapped (per established pattern).
|
|
$script:dgADMXCategoryPolicies.add_DoubleTapped((ConvertTo-AvaloniaEventScriptBlock {
|
|
if (-not $script:dgADMXCategoryPolicies.SelectedItem) { return }
|
|
Show-ADMXSettingPropertiesDialog $script:dgADMXCategoryPolicies.SelectedItem
|
|
}))
|
|
|
|
$mnu = $script:dgADMXCategoryPolicies.ContextMenu
|
|
if ($mnu) {
|
|
# Avalonia ContextMenu has Opening (cancellable) but the WPF Opened-
|
|
# to-toggle-IsEnabled pattern is fragile here anyway; the click
|
|
# handler already returns early if there's no SelectedItem, so no
|
|
# menu-open hook is needed.
|
|
$edit = $mnu.Items | Where-Object Name -eq 'mnuADMXSettingEdit' | Select-Object -First 1
|
|
if ($edit) {
|
|
$edit.add_Click((ConvertTo-AvaloniaEventScriptBlock {
|
|
if ($script:dgADMXCategoryPolicies.SelectedItem) {
|
|
Show-ADMXSettingPropertiesDialog $script:dgADMXCategoryPolicies.SelectedItem
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
# Pre-load Windows.adml for SUPPORTED_* string resolution (best-effort).
|
|
if (-not $script:_admxWindowsADML) {
|
|
$winADML = "$($env:WinDir)\PolicyDefinitions\en-US\Windows.adml"
|
|
if ([IO.File]::Exists($winADML)) {
|
|
try { [xml]$script:_admxWindowsADML = [IO.File]::ReadAllText($winADML) }
|
|
catch { Write-LogDebug "Failed to load Windows.adml: $($_.Exception.Message)" }
|
|
}
|
|
}
|
|
}
|
|
|
|
# ─── ADMX Import — SettingProperties dialog (Slice 5a3) ──────────────────────
|
|
#
|
|
# Avalonia port of Show-ADMXSettingPropertiesDialog + Set-ADMXSettingProperties +
|
|
# Set-ADMXElementsPanel + Save-ADMXSettings + Get-ADMXSettingsString from
|
|
# UI/WPF/Extensions/IntuneToolsUI.ps1. Differences from WPF:
|
|
# - {Binding ...} on PSCustomObject doesn't render in Avalonia. Form fields
|
|
# that bound to SupportedOn / Description / PolicyDefinition are now named
|
|
# TextBoxes; Set-ADMXSettingProperties pushes values imperatively.
|
|
# - WPF NumericUpDown.xaml replaced with [Avalonia.Controls.NumericUpDown];
|
|
# Get-ADMXSettingsString reads .Value directly (no .Children[0].Text dance).
|
|
# - WPF [System.Windows.LogicalTreeHelper]::FindLogicalNode replaced with a
|
|
# flat scan of $Item.ElementsPanel.Children by .Name (the elements grid is
|
|
# only one level deep so iteration is fine).
|
|
# - WPF DataGrid.CanUserAddRows is missing in Avalonia 11; listBox elements
|
|
# get inline Add / Remove buttons after the DataGrid so users can build
|
|
# the rows themselves.
|
|
# - WPF CollectionViewSource.GetDefaultView(...).Refresh() has no Avalonia
|
|
# equivalent; rebuild the ItemsSource ObservableCollection instead so the
|
|
# single edited row's status text updates.
|
|
|
|
function Show-ADMXSettingPropertiesDialog
|
|
{
|
|
param($SettingObj)
|
|
$ui = $script:UIProvider
|
|
if (-not $SettingObj) { return }
|
|
|
|
$form = $ui.GetXamlObject((Join-Path $script:AppUIRootFolder 'XAML/IntuneToolsADMXSettingProperties.axaml'))
|
|
if (-not $form) { return }
|
|
|
|
$script:_admxSettingsForm = $form
|
|
$script:_admxLblSettingName = (Get-AvaloniaHost)::FindByName($form, 'lblADMXSettingName')
|
|
$script:_admxGrdElements = (Get-AvaloniaHost)::FindByName($form, 'grdADMXElements')
|
|
$script:_admxRbEnabled = (Get-AvaloniaHost)::FindByName($form, 'rbADMXSettingEnabled')
|
|
$script:_admxRbDisabled = (Get-AvaloniaHost)::FindByName($form, 'rbADMXSettingDisabled')
|
|
$script:_admxRbNotConfigured = (Get-AvaloniaHost)::FindByName($form, 'rbADMXSettingNotConfigured')
|
|
$script:_admxTxtSettings = (Get-AvaloniaHost)::FindByName($form, 'txtADMXSettings')
|
|
$script:_admxTxtSettingName = (Get-AvaloniaHost)::FindByName($form, 'txtADMXSettingName')
|
|
$script:_admxTxtSettingComment = (Get-AvaloniaHost)::FindByName($form, 'txtADMXSettingComment')
|
|
$script:_admxTxtSupportedOn = (Get-AvaloniaHost)::FindByName($form, 'txtADMXSupportedOn')
|
|
$script:_admxTxtDescription = (Get-AvaloniaHost)::FindByName($form, 'txtADMXSettingDescription')
|
|
$script:_admxTxtPolicyDefinition = (Get-AvaloniaHost)::FindByName($form, 'txtADMXPolicyDefinition')
|
|
$script:_admxChkManualConfig = (Get-AvaloniaHost)::FindByName($form, 'chkADMXManualConfig')
|
|
$script:_admxBtnPrev = (Get-AvaloniaHost)::FindByName($form, 'btnADMXPreviousSetting')
|
|
$script:_admxBtnNext = (Get-AvaloniaHost)::FindByName($form, 'btnADMXNextSetting')
|
|
$script:_admxTcPolicyConfig = (Get-AvaloniaHost)::FindByName($form, 'tcADMXPolicyConfig')
|
|
$script:_admxTabSettings = (Get-AvaloniaHost)::FindByName($form, 'tabADMXSettings')
|
|
|
|
Set-ADMXSettingProperties $SettingObj
|
|
Set-ADMXSettingButtonsStatus
|
|
|
|
$ui.AddXamlEvent($form, 'btnADMXSettingsOK', 'Add_Click', ({
|
|
Save-ADMXSettings
|
|
Show-ModalObject
|
|
}))
|
|
|
|
# Capture the index BEFORE Save-ADMXSettings: it rebuilds the grid's
|
|
# ItemsSource (its refresh mechanism), which resets SelectedIndex to -1 -
|
|
# reading the index afterwards navigated to an invalid row and the dialog
|
|
# silently stayed put.
|
|
$ui.AddXamlEvent($form, 'btnADMXPreviousSetting', 'Add_Click', ({
|
|
$idx = $script:dgADMXCategoryPolicies.SelectedIndex
|
|
if ($idx -le 0) { return }
|
|
Save-ADMXSettings
|
|
$script:dgADMXCategoryPolicies.SelectedIndex = $idx - 1
|
|
Set-ADMXSettingButtonsStatus
|
|
Set-ADMXSettingProperties $script:dgADMXCategoryPolicies.SelectedItem
|
|
}))
|
|
|
|
$ui.AddXamlEvent($form, 'btnADMXNextSetting', 'Add_Click', ({
|
|
$idx = $script:dgADMXCategoryPolicies.SelectedIndex
|
|
$count = @($script:dgADMXCategoryPolicies.ItemsSource).Count
|
|
if ($idx -lt 0 -or $idx -ge ($count - 1)) { return }
|
|
Save-ADMXSettings
|
|
$script:dgADMXCategoryPolicies.SelectedIndex = $idx + 1
|
|
Set-ADMXSettingButtonsStatus
|
|
Set-ADMXSettingProperties $script:dgADMXCategoryPolicies.SelectedItem
|
|
}))
|
|
|
|
$ui.AddXamlEvent($form, 'btnADMXSettingsCancel', 'Add_Click', ({
|
|
if ($script:_admxGrdElements) { $script:_admxGrdElements.Children.Clear() }
|
|
$script:_admxSettingsForm = $null
|
|
Show-ModalObject
|
|
}))
|
|
|
|
# WPF used Add_Checked on each RadioButton; Avalonia ToggleButton fires
|
|
# IsCheckedChanged regardless of direction, so we gate on IsChecked inside.
|
|
$script:_admxRbEnabled.add_IsCheckedChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
if ($script:_admxRbEnabled.IsChecked -eq $true) {
|
|
$script:_admxCurItemStatus = 1
|
|
Set-ADMXControlStatus
|
|
}
|
|
}))
|
|
$script:_admxRbDisabled.add_IsCheckedChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
if ($script:_admxRbDisabled.IsChecked -eq $true) {
|
|
$script:_admxCurItemStatus = 0
|
|
Set-ADMXControlStatus
|
|
}
|
|
}))
|
|
$script:_admxRbNotConfigured.add_IsCheckedChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
if ($script:_admxRbNotConfigured.IsChecked -eq $true) {
|
|
$script:_admxCurItemStatus = $null
|
|
Set-ADMXControlStatus
|
|
}
|
|
}))
|
|
|
|
$script:_admxChkManualConfig.add_Click((ConvertTo-AvaloniaEventScriptBlock { Set-ADMXControlStatus }))
|
|
|
|
$script:_admxTcPolicyConfig.add_SelectionChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
if ($E.AddedItems.Count -eq 0) { return }
|
|
if ($E.AddedItems[0] -eq $script:_admxTabSettings -and
|
|
$script:dgADMXCategoryPolicies.SelectedItem -and
|
|
$script:dgADMXCategoryPolicies.SelectedItem.ManualConfig -ne 1) {
|
|
$script:_admxTxtSettings.Text = Get-ADMXSettingsString $script:dgADMXCategoryPolicies.SelectedItem
|
|
}
|
|
}))
|
|
|
|
$ui.ShowModalForm($SettingObj.Name, $form, $true)
|
|
}
|
|
|
|
function Set-ADMXSettingButtonsStatus
|
|
{
|
|
if (-not $script:_admxBtnPrev -or -not $script:_admxBtnNext) { return }
|
|
$count = @($script:dgADMXCategoryPolicies.ItemsSource).Count
|
|
$script:_admxBtnPrev.IsEnabled = $script:dgADMXCategoryPolicies.SelectedIndex -gt 0
|
|
$script:_admxBtnNext.IsEnabled = $script:dgADMXCategoryPolicies.SelectedIndex -lt ($count - 1)
|
|
}
|
|
|
|
function Set-ADMXSettingProperties
|
|
{
|
|
param($SettingObj)
|
|
if (-not $SettingObj) { return }
|
|
|
|
$script:_admxGrdElements.Children.Clear()
|
|
$script:_admxCurItemStatus = $SettingObj.SettingStatus
|
|
|
|
switch ($SettingObj.SettingStatus) {
|
|
0 { $script:_admxRbDisabled.IsChecked = $true }
|
|
1 { $script:_admxRbEnabled.IsChecked = $true }
|
|
default { $script:_admxRbNotConfigured.IsChecked = $true }
|
|
}
|
|
|
|
$script:_admxLblSettingName.Content = "Setting: $($SettingObj.Name)"
|
|
$script:_admxTxtSettings.Text = [string]$SettingObj.PolicySettings
|
|
$script:_admxChkManualConfig.IsChecked = [bool]$SettingObj.ManualConfig
|
|
$script:_admxTxtSettingName.Text = [string]$SettingObj.OMAURIName
|
|
$script:_admxTxtSettingComment.Text = [string]$SettingObj.Description
|
|
|
|
if (-not $SettingObj.PolicyDefinition) {
|
|
$SettingObj.PolicyDefinition = Format-XML $SettingObj.Definition.OuterXml
|
|
}
|
|
$script:_admxTxtPolicyDefinition.Text = [string]$SettingObj.PolicyDefinition
|
|
|
|
if (-not $SettingObj.SupportedOn -and $SettingObj.Definition.supportedOn.ref) {
|
|
$supRef = $SettingObj.Definition.supportedOn.ref.Split(':')[-1]
|
|
$supObj = $script:_admxSupportedOn | Where-Object Id -eq $supRef
|
|
if ($supObj) { $SettingObj.SupportedOn = $supObj.DisplayName }
|
|
}
|
|
$script:_admxTxtSupportedOn.Text = [string]$SettingObj.SupportedOn
|
|
$script:_admxTxtDescription.Text = [string]$SettingObj.Description
|
|
|
|
Set-ADMXElementsPanel $SettingObj
|
|
Set-ADMXControlStatus
|
|
}
|
|
|
|
function Set-ADMXControlStatus
|
|
{
|
|
# Element grid editable only when policy is Enabled AND not in manual mode.
|
|
# Manual-mode TextBox is editable only when Enabled + manual mode.
|
|
$manual = ($script:_admxChkManualConfig.IsChecked -eq $true)
|
|
$script:_admxGrdElements.IsEnabled = ($script:_admxCurItemStatus -eq 1 -and -not $manual)
|
|
$script:_admxTxtSettings.IsReadOnly = (-not ($script:_admxCurItemStatus -eq 1 -and $manual))
|
|
}
|
|
|
|
function Save-ADMXSettings
|
|
{
|
|
$item = $script:dgADMXCategoryPolicies.SelectedItem
|
|
if (-not $item) { return }
|
|
|
|
if ($script:_admxChkManualConfig.IsChecked -eq $true) {
|
|
$item.PolicySettings = $script:_admxTxtSettings.Text
|
|
} else {
|
|
$item.PolicySettings = Get-ADMXSettingsString $item
|
|
}
|
|
$item.ManualConfig = if ($script:_admxChkManualConfig.IsChecked -eq $true) { 1 } else { 0 }
|
|
$item.SettingStatus = $script:_admxCurItemStatus
|
|
$item.OMAURIName = $script:_admxTxtSettingName.Text
|
|
Set-ADMXSettingStatusText $item
|
|
|
|
# Avalonia has no CollectionViewSource.Refresh; rebuild the ObservableCollection
|
|
# to push the new SettingStatusText / PolicySettings into the grid cells.
|
|
$current = @($script:dgADMXCategoryPolicies.ItemsSource)
|
|
$script:dgADMXCategoryPolicies.ItemsSource = [System.Collections.ObjectModel.ObservableCollection[object]]::new($current)
|
|
|
|
$script:_admxGrdElements.Children.Clear()
|
|
}
|
|
|
|
function Add-ADMXGridObject
|
|
{
|
|
# Avalonia equivalent of WPF Add-GridObject — appends an Auto-height row to
|
|
# the target Grid, sets Grid.Row on the control, and adds it.
|
|
param($Grid, $Obj)
|
|
|
|
$rd = [Avalonia.Controls.RowDefinition]::new()
|
|
$rd.Height = [Avalonia.Controls.GridLength]::Auto
|
|
[Avalonia.Controls.Grid]::SetRow($Obj, $Grid.RowDefinitions.Count)
|
|
[void]$Grid.RowDefinitions.Add($rd)
|
|
[void]$Grid.Children.Add($Obj)
|
|
}
|
|
|
|
function Add-ADMXListEditorButtons
|
|
{
|
|
# Avalonia DataGrid lacks CanUserAddRows / CanUserDeleteRows — mount an
|
|
# Add / Remove button bar under the DataGrid so listBox elements stay
|
|
# usable. Buttons close over $DataGrid via .Tag.
|
|
param($Grid, $DataGrid)
|
|
|
|
$sp = [Avalonia.Controls.StackPanel]::new()
|
|
$sp.Orientation = [Avalonia.Layout.Orientation]::Horizontal
|
|
$sp.HorizontalAlignment = [Avalonia.Layout.HorizontalAlignment]::Right
|
|
$sp.Margin = [Avalonia.Thickness]::new(0, 5, 0, 0)
|
|
|
|
$btnAdd = [Avalonia.Controls.Button]::new()
|
|
$btnAdd.Content = 'Add row'
|
|
$btnAdd.MinWidth = 80
|
|
$btnAdd.Tag = $DataGrid
|
|
$btnAdd.add_Click((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
$dg = $S.Tag
|
|
try { [void]$dg.CommitEdit([Avalonia.Controls.DataGridEditingUnit]::Cell, $true) } catch { }
|
|
try { [void]$dg.CommitEdit([Avalonia.Controls.DataGridEditingUnit]::Row, $true) } catch { }
|
|
|
|
$items = $dg.ItemsSource
|
|
if (-not ($items -is [System.Collections.ObjectModel.ObservableCollection[ADMXListValueItem]])) {
|
|
$items = New-ADMXListItemsCollection $items
|
|
$dg.ItemsSource = $items
|
|
}
|
|
$items.Add([ADMXListValueItem]::new())
|
|
$dg.SelectedIndex = $items.Count - 1
|
|
}))
|
|
|
|
$btnRemove = [Avalonia.Controls.Button]::new()
|
|
$btnRemove.Content = 'Remove'
|
|
$btnRemove.MinWidth = 80
|
|
$btnRemove.Margin = [Avalonia.Thickness]::new(5, 0, 0, 0)
|
|
$btnRemove.Tag = $DataGrid
|
|
$btnRemove.add_Click((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
$dg = $S.Tag
|
|
if ($dg.SelectedIndex -lt 0) { return }
|
|
try { [void]$dg.CommitEdit([Avalonia.Controls.DataGridEditingUnit]::Cell, $true) } catch { }
|
|
try { [void]$dg.CommitEdit([Avalonia.Controls.DataGridEditingUnit]::Row, $true) } catch { }
|
|
|
|
$items = $dg.ItemsSource
|
|
if (-not ($items -is [System.Collections.ObjectModel.ObservableCollection[ADMXListValueItem]])) {
|
|
$items = New-ADMXListItemsCollection $items
|
|
$dg.ItemsSource = $items
|
|
}
|
|
$items.RemoveAt($dg.SelectedIndex)
|
|
}))
|
|
|
|
[void]$sp.Children.Add($btnAdd)
|
|
[void]$sp.Children.Add($btnRemove)
|
|
Add-ADMXGridObject $Grid $sp
|
|
}
|
|
|
|
function Set-ADMXComboDisplayMember
|
|
{
|
|
# ADMX presentation combos are built in code, so they get their display
|
|
# field here instead of in .axaml. Items are [SettingsListItem].
|
|
param($ComboBox)
|
|
|
|
if (-not $ComboBox) { return }
|
|
$ComboBox.DisplayMemberBinding = [Avalonia.Data.Binding]::new('Name')
|
|
|
|
# Avalonia's ComboBox defaults to HorizontalAlignment=Left (TextBox and
|
|
# most other controls default to Stretch), so a combo dropped into the
|
|
# elements grid collapses to ~64px in a 424px column instead of filling
|
|
# it like the WPF original. Measured 2026-08-27.
|
|
$ComboBox.HorizontalAlignment = [Avalonia.Layout.HorizontalAlignment]::Stretch
|
|
}
|
|
|
|
function New-ADMXListItemsCollection
|
|
{
|
|
param($Items)
|
|
|
|
$collection = [System.Collections.ObjectModel.ObservableCollection[ADMXListValueItem]]::new()
|
|
foreach ($item in @($Items)) {
|
|
if ($null -eq $item) { continue }
|
|
if ($item -is [ADMXListValueItem]) {
|
|
$collection.Add($item)
|
|
}
|
|
else {
|
|
$row = [ADMXListValueItem]::new()
|
|
if ($null -ne $item.Key) { $row.Key = [string]$item.Key }
|
|
if ($null -ne $item.Value) { $row.Value = [string]$item.Value }
|
|
$collection.Add($row)
|
|
}
|
|
}
|
|
return ,$collection
|
|
}
|
|
|
|
function Set-ADMXListGridDefaults
|
|
{
|
|
param($DataGrid)
|
|
|
|
if (-not $DataGrid) { return }
|
|
$DataGrid.CanUserSortColumns = $false
|
|
$DataGrid.CanUserResizeColumns = $true
|
|
$DataGrid.AutoGenerateColumns = $false
|
|
$DataGrid.IsReadOnly = $false
|
|
$DataGrid.HorizontalAlignment = [Avalonia.Layout.HorizontalAlignment]::Stretch
|
|
$DataGrid.ColumnWidth = [Avalonia.Controls.DataGridLength]::new(1, [Avalonia.Controls.DataGridLengthUnitType]::Star)
|
|
$DataGrid.MinHeight = 72
|
|
$DataGrid.MaxHeight = 120
|
|
$DataGrid.RowHeight = 28
|
|
$DataGrid.ColumnHeaderHeight = 28
|
|
$DataGrid.VerticalScrollBarVisibility = [Avalonia.Controls.Primitives.ScrollBarVisibility]::Auto
|
|
$DataGrid.HorizontalScrollBarVisibility = [Avalonia.Controls.Primitives.ScrollBarVisibility]::Disabled
|
|
}
|
|
|
|
function Set-ADMXElementsPanel
|
|
{
|
|
# Build per-element editor controls for a policy. Layout matches the WPF
|
|
# version 1:1; only the control types and grid attached props change.
|
|
param($Item)
|
|
|
|
if (-not $Item.Definition.elements) { return }
|
|
|
|
if (-not $Item.ElementsPanel) {
|
|
$grd = [Avalonia.Controls.Grid]::new()
|
|
$grd.HorizontalAlignment = [Avalonia.Layout.HorizontalAlignment]::Stretch
|
|
$presentation = Get-ADMXPresentationNode $Item
|
|
|
|
if ($presentation) {
|
|
foreach ($presentationNode in $presentation.ChildNodes) {
|
|
$ctrl = $null
|
|
$elementNode = $Item.Definition.elements.ChildNodes | Where-Object id -eq $presentationNode.refId
|
|
|
|
if ($presentationNode.Label.'#text') { $stringLabel = $presentationNode.Label.'#text' }
|
|
elseif ($presentationNode.Label) { $stringLabel = $presentationNode.Label }
|
|
else { $stringLabel = $presentationNode.'#text' }
|
|
|
|
if ($stringLabel -and $presentationNode.LocalName -ne 'CheckBox') {
|
|
$ctrl = [Avalonia.Controls.TextBlock]::new()
|
|
$ctrl.Text = [string]$stringLabel
|
|
Add-ADMXGridObject $grd $ctrl
|
|
}
|
|
|
|
if ($presentationNode.LocalName -eq 'text') { continue }
|
|
|
|
if ($ctrl) { $ctrl.Margin = [Avalonia.Thickness]::new(0, 5, 0, 0) }
|
|
|
|
switch ($presentationNode.LocalName)
|
|
{
|
|
'textbox' {
|
|
$ctrl = [Avalonia.Controls.TextBox]::new()
|
|
if ($null -ne $presentationNode.defaultValue) { $ctrl.Text = [string]$presentationNode.defaultValue }
|
|
}
|
|
{ $_ -in @('DecimalTextBox','LongDecimalTextBox') } {
|
|
$ctrl = [Avalonia.Controls.NumericUpDown]::new()
|
|
$ctrl.Minimum = [decimal](?? $elementNode.minValue 0)
|
|
$ctrl.Maximum = [decimal](?? $elementNode.maxValue 9999)
|
|
$ctrl.Increment = [decimal](?? $elementNode.SpinStep 1)
|
|
$ctrl.FormatString = '0'
|
|
if ($null -ne $presentationNode.defaultValue) {
|
|
try { $ctrl.Value = [decimal]$presentationNode.defaultValue } catch { }
|
|
}
|
|
}
|
|
'multiText' {
|
|
$ctrl = [Avalonia.Controls.TextBox]::new()
|
|
$ctrl.Height = 100
|
|
$ctrl.AcceptsReturn = $true
|
|
}
|
|
'CheckBox' {
|
|
$ctrl = [Avalonia.Controls.CheckBox]::new()
|
|
$ctrl.Content = [string]$stringLabel
|
|
if ($presentationNode.defaultChecked -eq $true) { $ctrl.IsChecked = $true }
|
|
}
|
|
{ $_ -in @('ComboBox','DropdownList') } {
|
|
$ctrl = [Avalonia.Controls.ComboBox]::new()
|
|
Set-ADMXComboDisplayMember $ctrl
|
|
|
|
$valItems = @()
|
|
foreach ($valItem in $elementNode.ChildNodes) {
|
|
$displayName = Get-ADMXADMLString $valItem
|
|
$value = $null
|
|
if ($valItem.value.decimal.value) { $value = $valItem.value.decimal.value }
|
|
elseif ($valItem.value.longDecimal) { $value = (?? $valItem.value.longDecimal.'#text' $valItem.value.longDecimal) }
|
|
elseif ($valItem.value.string) { $value = (?? $valItem.value.string.'#text' $valItem.value.string) }
|
|
else {
|
|
Write-Log "Unsupported value type for $($elementNode.Id): $($valItem.value.InnerXml)" 2
|
|
$value = '<SET MANUALLY!!!>'
|
|
}
|
|
$valItems += [SettingsListItem]@{ Name = [string]$displayName; Value = $value }
|
|
}
|
|
# Cast back to a typed array, don't just pipe. Sort-Object
|
|
# collapses a single item to a scalar (ItemsSource then
|
|
# throws) and re-wraps every item in a PSObject, which the
|
|
# binder cannot see through - DisplayMemberBinding would
|
|
# render blank rows. The [SettingsListItem[]] cast fixes both.
|
|
if ($presentationNode.NoSort -ne 'true') {
|
|
$valItems = [SettingsListItem[]]@($valItems | Sort-Object -Property Name)
|
|
}
|
|
$ctrl.ItemsSource = $valItems
|
|
if ($null -ne $presentationNode.defaultItem) {
|
|
try { $ctrl.SelectedIndex = [int]$presentationNode.defaultItem } catch { }
|
|
}
|
|
}
|
|
'listBox' {
|
|
$ctrl = [Avalonia.Controls.DataGrid]::new()
|
|
Set-ADMXListGridDefaults $ctrl
|
|
|
|
$col = [Avalonia.Controls.DataGridTextColumn]::new()
|
|
$col.Header = 'Value Name'
|
|
$col.Width = [Avalonia.Controls.DataGridLength]::new(1, [Avalonia.Controls.DataGridLengthUnitType]::Star)
|
|
$col.Binding = [Avalonia.Data.Binding]::new('Key')
|
|
$col.Binding.Mode = [Avalonia.Data.BindingMode]::TwoWay
|
|
$col.Binding.UpdateSourceTrigger = [Avalonia.Data.UpdateSourceTrigger]::PropertyChanged
|
|
if ($elementNode.explicitValue -ne 'true') { $col.IsVisible = $false }
|
|
[void]$ctrl.Columns.Add($col)
|
|
|
|
$col = [Avalonia.Controls.DataGridTextColumn]::new()
|
|
$col.Header = 'Value'
|
|
$col.Width = [Avalonia.Controls.DataGridLength]::new(1, [Avalonia.Controls.DataGridLengthUnitType]::Star)
|
|
$col.Binding = [Avalonia.Data.Binding]::new('Value')
|
|
$col.Binding.Mode = [Avalonia.Data.BindingMode]::TwoWay
|
|
$col.Binding.UpdateSourceTrigger = [Avalonia.Data.UpdateSourceTrigger]::PropertyChanged
|
|
[void]$ctrl.Columns.Add($col)
|
|
|
|
$ctrl.ItemsSource = New-ADMXListItemsCollection
|
|
}
|
|
default {
|
|
Write-Log "Unsupported presentation control: $($presentationNode.LocalName) (refId: $($presentationNode.refId))" 2
|
|
continue
|
|
}
|
|
}
|
|
|
|
if ($ctrl) {
|
|
Add-ADMXGridObject $grd $ctrl
|
|
if ($presentationNode.refId) {
|
|
$ctrl.Tag = $elementNode
|
|
$ctrl.Name = $presentationNode.refId
|
|
}
|
|
if ($presentationNode.LocalName -eq 'listBox') {
|
|
Add-ADMXListEditorButtons $grd $ctrl
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# Fallback: no presentation. Best-effort controls straight from <elements>.
|
|
Write-Log "No presentation found for $($Item.Definition.Name); building fallback editor" 2
|
|
foreach ($elementNode in $Item.Definition.elements.ChildNodes) {
|
|
try {
|
|
$ctrl = $null
|
|
switch ($elementNode.LocalName) {
|
|
'text' { $ctrl = [Avalonia.Controls.TextBox]::new() }
|
|
'multiText' {
|
|
$ctrl = [Avalonia.Controls.TextBox]::new()
|
|
$ctrl.Height = 100
|
|
$ctrl.AcceptsReturn = $true
|
|
}
|
|
'enum' {
|
|
$ctrl = [Avalonia.Controls.ComboBox]::new()
|
|
Set-ADMXComboDisplayMember $ctrl
|
|
|
|
$valItems = @()
|
|
foreach ($valItem in $elementNode.ChildNodes) {
|
|
$value = $null
|
|
if ($valItem.value.decimal.value) { $value = $valItem.value.decimal.value }
|
|
elseif ($valItem.value.string) { $value = (?? $valItem.value.string.'#text' $valItem.value.string) }
|
|
else { $value = '<SET MANUALLY!!!>' }
|
|
$valItems += [SettingsListItem]@{ Name = [string](Get-ADMXADMLString $valItem); Value = $value }
|
|
}
|
|
$ctrl.ItemsSource = $valItems
|
|
}
|
|
'list' {
|
|
$ctrl = [Avalonia.Controls.DataGrid]::new()
|
|
Set-ADMXListGridDefaults $ctrl
|
|
$col = [Avalonia.Controls.DataGridTextColumn]::new()
|
|
$col.Header = 'Value Name'
|
|
$col.Width = [Avalonia.Controls.DataGridLength]::new(1, [Avalonia.Controls.DataGridLengthUnitType]::Star)
|
|
$col.Binding = [Avalonia.Data.Binding]::new('Key')
|
|
$col.Binding.Mode = [Avalonia.Data.BindingMode]::TwoWay
|
|
$col.Binding.UpdateSourceTrigger = [Avalonia.Data.UpdateSourceTrigger]::PropertyChanged
|
|
if ($elementNode.explicitValue -ne 'true') { $col.IsVisible = $false }
|
|
[void]$ctrl.Columns.Add($col)
|
|
$col = [Avalonia.Controls.DataGridTextColumn]::new()
|
|
$col.Header = 'Value'
|
|
$col.Width = [Avalonia.Controls.DataGridLength]::new(1, [Avalonia.Controls.DataGridLengthUnitType]::Star)
|
|
$col.Binding = [Avalonia.Data.Binding]::new('Value')
|
|
$col.Binding.Mode = [Avalonia.Data.BindingMode]::TwoWay
|
|
$col.Binding.UpdateSourceTrigger = [Avalonia.Data.UpdateSourceTrigger]::PropertyChanged
|
|
[void]$ctrl.Columns.Add($col)
|
|
$ctrl.ItemsSource = New-ADMXListItemsCollection
|
|
}
|
|
'decimal' {
|
|
$ctrl = [Avalonia.Controls.NumericUpDown]::new()
|
|
$ctrl.FormatString = '0'
|
|
}
|
|
'boolean' {
|
|
$ctrl = [Avalonia.Controls.CheckBox]::new()
|
|
}
|
|
default {
|
|
Write-Log "Element type not supported in fallback: $($elementNode.LocalName)" 2
|
|
continue
|
|
}
|
|
}
|
|
|
|
if ($null -eq $ctrl) { continue }
|
|
|
|
$displayName = Get-ADMXADMLPresentationString $presentation $elementNode
|
|
if ($displayName) {
|
|
$tb = [Avalonia.Controls.TextBlock]::new()
|
|
if ($grd.RowDefinitions.Count -gt 0) {
|
|
$tb.Margin = [Avalonia.Thickness]::new(0, 5, 0, 0)
|
|
}
|
|
$tb.Text = [string]$displayName
|
|
Add-ADMXGridObject $grd $tb
|
|
}
|
|
|
|
$ctrl.Tag = $elementNode
|
|
$ctrl.Name = $elementNode.Id
|
|
Add-ADMXGridObject $grd $ctrl
|
|
|
|
if ($elementNode.LocalName -eq 'list') {
|
|
Add-ADMXListEditorButtons $grd $ctrl
|
|
}
|
|
}
|
|
catch {
|
|
Write-LogError "Failed to add ADMX element $($elementNode.LocalName) with id $($elementNode.id)" $_.Exception
|
|
}
|
|
}
|
|
}
|
|
|
|
# Trailing row so the grid stretches; harmless if empty.
|
|
[void]$grd.RowDefinitions.Add([Avalonia.Controls.RowDefinition]::new())
|
|
$Item.ElementsPanel = $grd
|
|
}
|
|
|
|
if ($Item.ElementsPanel) {
|
|
# Detach from previous parent — Avalonia controls can only have one
|
|
# logical parent, and re-opening the dialog re-mounts the same panel.
|
|
if ($Item.ElementsPanel.Parent) {
|
|
$oldParent = $Item.ElementsPanel.Parent
|
|
if ($oldParent.Children -and $oldParent.Children.Contains($Item.ElementsPanel)) {
|
|
[void]$oldParent.Children.Remove($Item.ElementsPanel)
|
|
}
|
|
}
|
|
[void]$script:_admxGrdElements.Children.Add($Item.ElementsPanel)
|
|
}
|
|
}
|
|
|
|
function Get-ADMXSettingsString
|
|
{
|
|
# Build the OMA-URI <data id=... value=.../> XML from live element controls.
|
|
# Empty values are skipped (with a log line for required ones). Returns the
|
|
# joined string or $null if the policy isn't Enabled.
|
|
param($Item)
|
|
|
|
if (-not $Item -or -not $Item.Definition.elements) { return }
|
|
if ($script:_admxCurItemStatus -ne 1) {
|
|
$Item.PolicySettings = $null
|
|
return
|
|
}
|
|
if (-not $Item.ElementsPanel) { return }
|
|
|
|
$policySettings = @()
|
|
foreach ($elementNode in $Item.Definition.elements.ChildNodes) {
|
|
$ctrl = $Item.ElementsPanel.Children | Where-Object { $_.Name -eq $elementNode.Id } | Select-Object -First 1
|
|
if (-not $ctrl) {
|
|
Write-Log "Could not find a control with id $($elementNode.Id)" 3
|
|
continue
|
|
}
|
|
|
|
$ctrlValue = $null
|
|
switch ($elementNode.LocalName) {
|
|
'text' { $ctrlValue = $ctrl.Text }
|
|
'multiText' { $ctrlValue = ($ctrl.Text -replace [Environment]::NewLine, '') }
|
|
'enum' {
|
|
if ($ctrl.SelectedItem) { $ctrlValue = $ctrl.SelectedItem.Value }
|
|
}
|
|
'list' {
|
|
try { [void]$ctrl.CommitEdit([Avalonia.Controls.DataGridEditingUnit]::Cell, $true) } catch { }
|
|
try { [void]$ctrl.CommitEdit([Avalonia.Controls.DataGridEditingUnit]::Row, $true) } catch { }
|
|
|
|
$i = 1
|
|
$arr = @()
|
|
foreach ($kv in $ctrl.ItemsSource) {
|
|
if (-not $kv.Value) { continue }
|
|
$arr += "$((?? $kv.Key $i))$($kv.Value)"
|
|
$i++
|
|
}
|
|
$ctrlValue = $arr -join ''
|
|
}
|
|
default {
|
|
if ($elementNode.LocalName -eq 'decimal' -or $ctrl.Tag.LocalName -eq 'longDecimal') {
|
|
if ($ctrl -is [Avalonia.Controls.NumericUpDown]) {
|
|
if ($null -ne $ctrl.Value) { $ctrlValue = $ctrl.Value.ToString() }
|
|
} else {
|
|
$ctrlValue = $ctrl.Text
|
|
}
|
|
}
|
|
elseif ($elementNode.LocalName -eq 'boolean') {
|
|
if ($ctrl -is [Avalonia.Controls.CheckBox]) {
|
|
$ctrlValue = if ($ctrl.IsChecked -eq $true) { '1' } else { '0' }
|
|
}
|
|
elseif ($ctrl -is [Avalonia.Controls.ComboBox]) {
|
|
if ($ctrl.SelectedItem) { $ctrlValue = $ctrl.SelectedItem.Value }
|
|
}
|
|
else {
|
|
Write-Log "Boolean element type not supported: $($elementNode.LocalName)" 2
|
|
continue
|
|
}
|
|
}
|
|
else {
|
|
Write-Log "Element type not supported: $($elementNode.LocalName)" 2
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-not $ctrlValue) {
|
|
if ($elementNode.required -eq $true) {
|
|
Write-Log "Required value is missing for $($elementNode.Id)" 3
|
|
} else {
|
|
Write-Log "Value not set for $($elementNode.Id) - value will not be added"
|
|
}
|
|
continue
|
|
}
|
|
|
|
$policySettings += "<data id=`"$($elementNode.Id)`" value=`"$ctrlValue`"/>"
|
|
}
|
|
return ($policySettings -join [Environment]::NewLine)
|
|
}
|
|
|
|
# ─── ADMX Import — Intune ingestion (Slice 5a4) ───────────────────────────────
|
|
#
|
|
# Direct port of UI/WPF/Extensions/IntuneToolsUI.ps1's Import-ADMXPolicyToIntune.
|
|
# No UI sub-dialog (the WPF tree's IntuneToolsADMXDriveMapping.xaml is dead
|
|
# code — never referenced from any handler), so the Avalonia version is a
|
|
# pure data-flow port: validate inputs → walk configured policies → POST a
|
|
# windows10CustomConfiguration to deviceManagement/deviceConfigurations.
|
|
function Import-ADMXPolicyToIntune
|
|
{
|
|
$ui = $script:UIProvider
|
|
if (-not $script:txtADMXProfileName.Text.Trim()) {
|
|
$ui.ShowMessageBox('Profile Name must be specified', 'ADMX Import', 'OK', 'Error')
|
|
return
|
|
}
|
|
if (-not $script:txtADMXPolicyFileName.Text.Trim()) {
|
|
$ui.ShowMessageBox('ADMX Policy Name must be specified', 'ADMX Import', 'OK', 'Error')
|
|
return
|
|
}
|
|
|
|
$configured = @()
|
|
foreach ($admxPolicy in @($script:_admxPolicies | Where-Object { $_.SettingStatus -in @(0,1) })) {
|
|
if ($admxPolicy.SettingStatus -eq 0) {
|
|
$policyValue = '<disabled/>'
|
|
}
|
|
else {
|
|
$policyValue = '<enabled/>'
|
|
if ($admxPolicy.PolicySettings) { $policyValue += "`n$($admxPolicy.PolicySettings)" }
|
|
}
|
|
|
|
if ((Get-SettingValue 'FormatOMAURI') -eq $true) {
|
|
$policyValue = Update-XmlFormatting $policyValue
|
|
}
|
|
|
|
$catPath = Get-ADMXCategoryOMAURIPath $admxPolicy.Definition.parentCategory.ref
|
|
$omaUri = "./$($admxPolicy.SettingClass)/Vendor/MSFT/Policy/Config/$($script:txtADMXPolicyAppName.Text)~Policy~$catPath/$($admxPolicy.Definition.name)"
|
|
|
|
$desc =
|
|
if ($admxPolicy.OMAURIDescription) { $admxPolicy.OMAURIDescription }
|
|
elseif ($admxPolicy.Description -and $admxPolicy.Description.Length -gt 1000) { $admxPolicy.Description.Substring(0, 1000) }
|
|
else { $admxPolicy.Description }
|
|
|
|
$configured += [PSCustomObject]@{
|
|
'@odata.type' = '#microsoft.graph.omaSettingString'
|
|
displayName = (?? $admxPolicy.OMAURIName $admxPolicy.Name)
|
|
description = $desc
|
|
omaUri = $omaUri
|
|
value = $policyValue
|
|
}
|
|
}
|
|
|
|
$intuneObj = [PSCustomObject]@{
|
|
'@odata.type' = '#microsoft.graph.windows10CustomConfiguration'
|
|
displayName = $script:txtADMXProfileName.Text
|
|
omaSettings = @()
|
|
roleScopeTagIds = @()
|
|
assignments = @()
|
|
}
|
|
if ($script:txtADMXProfileDescription.Text) {
|
|
$intuneObj | Add-Member -MemberType NoteProperty -Name 'description' -Value $script:txtADMXProfileDescription.Text
|
|
}
|
|
|
|
if ($script:chkADMXPolicyIngest.IsChecked) {
|
|
$xmlString = Format-XML $script:_admxXML
|
|
if ((Get-SettingValue 'FormatOMAURI') -eq $true) {
|
|
$xmlString = Update-XmlFormatting $xmlString
|
|
}
|
|
|
|
$ingestName = if ($script:txtADMXPolicyIngestName.Text) { $script:txtADMXPolicyIngestName.Text }
|
|
else { "$($script:currentADMXFile.Name) Ingestion" }
|
|
|
|
$intuneObj.omaSettings += [PSCustomObject]@{
|
|
'@odata.type' = '#microsoft.graph.omaSettingString'
|
|
displayName = $ingestName
|
|
description = $null
|
|
omaUri = "./Device/Vendor/MSFT/Policy/ConfigOperations/ADMXInstall/$($script:txtADMXPolicyAppName.Text)/Policy/$($script:txtADMXPolicyFileName.Text)"
|
|
value = $xmlString
|
|
}
|
|
}
|
|
|
|
$intuneObj.omaSettings += $configured
|
|
$json = $intuneObj | ConvertTo-Json -Depth 20
|
|
|
|
$tokenId = Get-DefaultTokenId
|
|
$resp = Invoke-MSGraphAPI -Url 'deviceManagement/deviceConfigurations' -Content $json -HttpMethod 'POST' -TokenId $tokenId -FullResponseObject
|
|
|
|
if ($resp -and $resp.Success) {
|
|
Write-Log "Device configuration profile '$($intuneObj.displayName)' created with id $($resp.Content.Id)"
|
|
$ui.ShowMessageBox("Profile '$($intuneObj.displayName)' created successfully.", 'ADMX Import', 'OK', 'Information')
|
|
}
|
|
else {
|
|
$errInfo = if ($resp) { "$($resp.StatusCode) $($resp.StatusDescription)" } else { 'no response' }
|
|
Write-Log "Failed to create device configuration profile '$($intuneObj.displayName)' - $errInfo" 3
|
|
$ui.ShowMessageBox("Failed to create device configuration profile '$($intuneObj.displayName)'.`n`n$errInfo`n`nCheck the log for details.", 'ADMX Import', 'OK', 'Error')
|
|
}
|
|
}
|
|
|
|
# ─── Reg Values tool — Slice 5b1 (main panel + classes wired) ────────────────
|
|
#
|
|
# Avalonia port of UI/WPF/Extensions/IntuneToolsUI.ps1 Show-ADMXRegValuesTool.
|
|
# The C# class definitions (ADMXRegPolicyElement / ADMXRegPolicy /
|
|
# ADMXRegProfile) are kept verbatim — they're shared CLR types backing the
|
|
# DataContext bindings. ADMXRegProfile.ADMXPolicies is an
|
|
# ObservableCollection<ADMXRegPolicy>, which Avalonia's DataGrid binds to
|
|
# without the PSCustomObject quirk (see [[avalonia-binding-needs-clr-types]]).
|
|
#
|
|
# Differences from WPF:
|
|
# - $panel.FindName -> (Get-AvaloniaHost)::FindByName.
|
|
# - cbADMXRegPolicyType has no DisplayMemberPath / SelectedValuePath; it gets
|
|
# [SettingsListItem] items + an ItemTemplate via LoadXaml, and a
|
|
# SelectionChanged handler pushes the chosen Value onto DataContext.PolicyType.
|
|
# - WPF MouseDoubleClick -> Avalonia DoubleTapped.
|
|
# - WPF Add_Opened on the ContextMenu (used to gate Edit's IsEnabled) is
|
|
# dropped — the click handler already returns early when SelectedItem is
|
|
# $null, so no menu-open hook is needed.
|
|
# - Show-ADMXRegSettingsDialog / Import-ADMXRegProfileToIntune are stubbed
|
|
# pending slice 5b2.
|
|
|
|
function Show-ADMXRegValuesTool
|
|
{
|
|
$ui = $script:UIProvider
|
|
if (-not $script:grdToolsMain) { return }
|
|
$script:grdToolsMain.Children.Clear()
|
|
|
|
Add-ADMXRegClasses
|
|
|
|
$panel = $ui.GetXamlObject((Join-Path $script:AppUIRootFolder 'XAML/IntuneToolsADMXRegValues.axaml'))
|
|
if (-not $panel) {
|
|
Write-Log "Failed to load IntuneToolsADMXRegValues.axaml" 3
|
|
Set-IntuneToolPlaceholder 'Reg Values'
|
|
return
|
|
}
|
|
|
|
$script:_admxRegValuesPanel = $panel
|
|
$script:cbADMXRegPolicyType = (Get-AvaloniaHost)::FindByName($panel, 'cbADMXRegPolicyType')
|
|
$script:dgADMXRegAddedPolicies = (Get-AvaloniaHost)::FindByName($panel, 'dgADMXRegAddedPolicies')
|
|
$script:mnuADMXRegPoliciesCtx = (Get-AvaloniaHost)::FindByName($panel, 'mnuADMXRegPoliciesContextMenu')
|
|
$script:mnuADMXRegPolicyEdit = (Get-AvaloniaHost)::FindByName($panel, 'mnuADMXRegPolicyEdit')
|
|
|
|
$panel.DataContext = [ADMXRegProfile]::new()
|
|
|
|
if ($script:cbADMXRegPolicyType) {
|
|
$script:cbADMXRegPolicyType.ItemsSource = @(
|
|
[SettingsListItem]@{ Name = 'Policy'; Value = 'Policy' }
|
|
[SettingsListItem]@{ Name = 'Preference'; Value = 'Preference' }
|
|
)
|
|
|
|
# Initial selection mirrors the DataContext default (PolicyType = "Policy").
|
|
$current = [string]$panel.DataContext.PolicyType
|
|
$sel = $script:cbADMXRegPolicyType.ItemsSource | Where-Object { $_.Value -eq $current } | Select-Object -First 1
|
|
if (-not $sel) { $sel = $script:cbADMXRegPolicyType.ItemsSource[0] }
|
|
if ($sel) { $script:cbADMXRegPolicyType.SelectedItem = $sel }
|
|
|
|
$script:cbADMXRegPolicyType.add_SelectionChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
if ($S.SelectedItem -and $script:_admxRegValuesPanel.DataContext) {
|
|
$script:_admxRegValuesPanel.DataContext.PolicyType = [string]$S.SelectedItem.Value
|
|
}
|
|
}))
|
|
}
|
|
|
|
$ui.AddXamlEvent($panel, 'btnADMXRegClear', 'add_Click', ({
|
|
$script:_admxRegValuesPanel.DataContext = [ADMXRegProfile]::new()
|
|
if ($script:cbADMXRegPolicyType) {
|
|
$sel = $script:cbADMXRegPolicyType.ItemsSource | Where-Object { $_.Value -eq 'Policy' } | Select-Object -First 1
|
|
if ($sel) { $script:cbADMXRegPolicyType.SelectedItem = $sel }
|
|
}
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'btnADMXAddRegValue', 'add_Click', ({
|
|
Show-ADMXRegSettingsDialog
|
|
}))
|
|
|
|
$ui.AddXamlEvent($panel, 'btnADMXRegImport', 'add_Click', ({
|
|
Write-Status 'Import Reg Settings policy'
|
|
Import-ADMXRegProfileToIntune
|
|
Write-Status ''
|
|
}))
|
|
|
|
if ($script:dgADMXRegAddedPolicies) {
|
|
$script:dgADMXRegAddedPolicies.add_DoubleTapped((ConvertTo-AvaloniaEventScriptBlock {
|
|
if (-not $script:dgADMXRegAddedPolicies.SelectedItem) { return }
|
|
Show-ADMXRegSettingsDialog $script:dgADMXRegAddedPolicies.SelectedItem
|
|
}))
|
|
}
|
|
|
|
if ($script:mnuADMXRegPolicyEdit) {
|
|
$script:mnuADMXRegPolicyEdit.add_Click((ConvertTo-AvaloniaEventScriptBlock {
|
|
if (-not $script:dgADMXRegAddedPolicies.SelectedItem) { return }
|
|
Show-ADMXRegSettingsDialog $script:dgADMXRegAddedPolicies.SelectedItem
|
|
}))
|
|
}
|
|
|
|
$script:grdToolsMain.Children.Add($panel) | Out-Null
|
|
}
|
|
|
|
# ─── Reg Values — C# classes (INotifyPropertyChanged for live DataGrid binding) ─
|
|
|
|
# ─── Reg Values — registry-path safety list ───────────────────────────────────
|
|
#
|
|
# The MDM ADMX-ingest path can't write to certain Microsoft-owned roots. The
|
|
# original lists came from:
|
|
# https://docs.microsoft.com/en-us/windows/client-management/mdm/win32-and-centennial-app-policy-configuration
|
|
# Carried over verbatim so behaviour matches the WPF tool.
|
|
$script:_admxRegUnsupportedLocations = @(
|
|
'System'
|
|
'Software\Microsoft'
|
|
'Software\Policies\Microsoft'
|
|
)
|
|
$script:_admxRegUnsupportedOverride = @(
|
|
'Software\Policies\Microsoft\Office'
|
|
'Software\Microsoft\Office'
|
|
'Software\Microsoft\Windows\CurrentVersion\Explorer'
|
|
'Software\Microsoft\Internet Explorer'
|
|
'software\policies\microsoft\shared tools\proofing tools'
|
|
'software\policies\microsoft\imejp'
|
|
'software\policies\microsoft\ime\shared'
|
|
'software\policies\microsoft\shared tools\graphics filters'
|
|
'software\policies\microsoft\windows\currentversion\explorer'
|
|
'software\policies\microsoft\softwareprotectionplatform'
|
|
'software\policies\microsoft\officesoftwareprotectionplatform'
|
|
'software\policies\microsoft\windows\windows search\preferences'
|
|
'software\policies\microsoft\exchange'
|
|
'software\microsoft\shared tools\proofing tools'
|
|
'software\microsoft\shared tools\graphics filters'
|
|
'software\microsoft\windows\windows search\preferences'
|
|
'software\microsoft\exchange'
|
|
'software\policies\microsoft\vba\security'
|
|
'software\microsoft\onedrive'
|
|
'software\Microsoft\Edge'
|
|
'Software\Microsoft\EdgeUpdate'
|
|
)
|
|
|
|
# Template used to synthesize the ADMX XML payload for the ingest profile. One
|
|
# child <policy> is cloned per UI-defined ADMXRegPolicy; the seed entry is
|
|
# removed before the XML is serialised.
|
|
$script:_admxRegTemplate = @"
|
|
<policyDefinitions revision="1.0" schemaVersion="1.0">
|
|
<categories>
|
|
<category name="RegImport" />
|
|
</categories>
|
|
<policies>
|
|
<policy name="" class="" displayName="" explainText="" presentation="" key="" valueName="">
|
|
<parentCategory ref="RegImport" />
|
|
<supportedOn ref="windows:SUPPORTED_Windows7" />
|
|
<enabledValue>
|
|
<decimal value="1" />
|
|
</enabledValue>
|
|
<disabledValue>
|
|
<decimal value="0" />
|
|
</disabledValue>
|
|
<elements>
|
|
</elements>
|
|
</policy>
|
|
</policies>
|
|
</policyDefinitions>
|
|
"@
|
|
|
|
function Get-ADMXRegIsKeySupported
|
|
{
|
|
param($RegKey)
|
|
|
|
$ui = $script:UIProvider
|
|
if (-not $RegKey) { return $true }
|
|
|
|
$tmpPath = $RegKey.Trim('\') + '\'
|
|
|
|
$blockedSource = ''
|
|
foreach ($blockedPath in $script:_admxRegUnsupportedLocations) {
|
|
if ($tmpPath -like "$blockedPath\*") { $blockedSource = $blockedPath; break }
|
|
}
|
|
if ($blockedSource) {
|
|
foreach ($exemptPath in $script:_admxRegUnsupportedOverride) {
|
|
if ($tmpPath -like "$exemptPath\*") { $blockedSource = ''; break }
|
|
}
|
|
}
|
|
|
|
if ($blockedSource) {
|
|
$ui.ShowMessageBox("The registry key '$RegKey' is not supported.`n`nBlocked by root key: $blockedSource", 'Unsupported reg key', 'OK', 'Error')
|
|
return $false
|
|
}
|
|
return $true
|
|
}
|
|
|
|
# ─── Reg Values — settings dialog (Slice 5b2) ────────────────────────────────
|
|
#
|
|
# Avalonia port of UI/WPF/Extensions/IntuneToolsUI.ps1 Show-ADMXRegSettingsDialog.
|
|
# Differences from WPF:
|
|
# - $form.FindName -> (Get-AvaloniaHost)::FindByName.
|
|
# - The three ComboBoxes (Hive / PolicyStatus / DataType) use [SettingsListItem]
|
|
# + LoadXaml ItemTemplate (no DisplayMemberPath / SelectedValuePath); a
|
|
# SelectionChanged handler pushes the chosen Value onto
|
|
# $script:_admxRegCurrentPolicy / $script:_admxRegCurrentElement so the C#
|
|
# class properties stay in sync. Initial selection matches the live object.
|
|
# - DataContext is SECTIONED: grdADMXRegPolicy binds the ADMXRegPolicy and
|
|
# grdADMXRegPolicyElement binds the ADMXRegPolicyElement, because the
|
|
# Avalonia binder cannot traverse a PSCustomObject wrapper's NoteProperties
|
|
# (a form-level wrapper left every control in this dialog unbound).
|
|
# - WPF MouseDoubleClick -> Avalonia DoubleTapped on dgADMXRegAddedElements.
|
|
# - WPF Visibility = "Visible" / "Collapsed" -> IsVisible = $true / $false.
|
|
# - Two-state buttons (btnADMXRegElementAdd / btnADMXRegElementNew) toggle via
|
|
# IsVisible rather than the WPF Visibility enum.
|
|
|
|
function Show-ADMXRegSettingsDialog
|
|
{
|
|
param($RegPolicy)
|
|
|
|
$ui = $script:UIProvider
|
|
$form = $ui.GetXamlObject((Join-Path $script:AppUIRootFolder 'XAML/IntuneToolsADMXAddRegPolicy.axaml'))
|
|
if (-not $form) { return }
|
|
|
|
$script:_admxRegPoliciesForm = $form
|
|
|
|
# New vs edit: when invoked from the "Add" button we get $null; double-click
|
|
# on a row passes the existing ADMXRegPolicy.
|
|
$newPolicy = -not $RegPolicy
|
|
if ($newPolicy) { $RegPolicy = [ADMXRegPolicy]::new() }
|
|
$script:_admxRegIsNewPolicy = $newPolicy
|
|
|
|
$newElement = [ADMXRegPolicyElement]::new()
|
|
# SECTIONED DataContexts, not a PSCustomObject wrapper: the Avalonia binder
|
|
# cannot traverse PSCustomObject NoteProperties, so a form-level
|
|
# {Binding RegPolicy.X} context left EVERY control in this dialog unbound
|
|
# (typed values never reached the object). Each grid gets the real CLR
|
|
# object it edits; both classes are C# with INPC.
|
|
$script:_admxRegCurrentPolicy = $RegPolicy
|
|
$script:_admxRegCurrentElement = $newElement
|
|
$grdPolicy = (Get-AvaloniaHost)::FindByName($form, 'grdADMXRegPolicy')
|
|
$grdElement = (Get-AvaloniaHost)::FindByName($form, 'grdADMXRegPolicyElement')
|
|
if ($grdPolicy) { $grdPolicy.DataContext = $RegPolicy }
|
|
if ($grdElement) { $grdElement.DataContext = $newElement }
|
|
$script:_admxRegGrdPolicy = $grdPolicy
|
|
$script:_admxRegGrdElement = $grdElement
|
|
# The elements grid binds the policy's real ObservableCollection directly
|
|
# (its {Binding PolicyElements} now resolves against no context of its own).
|
|
$dgElementsEarly = (Get-AvaloniaHost)::FindByName($form, 'dgADMXRegAddedElements')
|
|
if ($dgElementsEarly) { $dgElementsEarly.ItemsSource = $RegPolicy.PolicyElements }
|
|
|
|
# Cache child controls used by event handlers and the show/hide helper.
|
|
$script:_admxRegCbHive = (Get-AvaloniaHost)::FindByName($form, 'cbADMXRegHive')
|
|
$script:_admxRegCbPolicyStatus = (Get-AvaloniaHost)::FindByName($form, 'cbADMXRegPolicyStatus')
|
|
$script:_admxRegCbDataType = (Get-AvaloniaHost)::FindByName($form, 'cbADMXRegElementDataType')
|
|
$script:_admxRegTxtElementKey = (Get-AvaloniaHost)::FindByName($form, 'txtADMXRegElementKey')
|
|
$script:_admxRegTxtElementValName = (Get-AvaloniaHost)::FindByName($form, 'txtADMXRegElementValueName')
|
|
$script:_admxRegTxtSeparator = (Get-AvaloniaHost)::FindByName($form, 'txtADMXRegAttributeValueSeparator')
|
|
$script:_admxRegSpSeparator = (Get-AvaloniaHost)::FindByName($form, 'spADMXRegAttributeValueSeparator')
|
|
$script:_admxRegSpValuePrefix = (Get-AvaloniaHost)::FindByName($form, 'spADMXRegAttributeValuePrefix')
|
|
$script:_admxRegTxtValuePrefix = (Get-AvaloniaHost)::FindByName($form, 'txtADMXRegAttributeValuePrefix')
|
|
$script:_admxRegSpExpandable = (Get-AvaloniaHost)::FindByName($form, 'spADMXRegAttributeExpandable')
|
|
$script:_admxRegChkExpandable = (Get-AvaloniaHost)::FindByName($form, 'chkADMXRegAttributeExpandable')
|
|
$script:_admxRegSpAdditive = (Get-AvaloniaHost)::FindByName($form, 'spADMXRegAttributeAdditive')
|
|
$script:_admxRegChkAdditive = (Get-AvaloniaHost)::FindByName($form, 'chkADMXRegAttributeAdditive')
|
|
$script:_admxRegBtnElementAdd = (Get-AvaloniaHost)::FindByName($form, 'btnADMXRegElementAdd')
|
|
$script:_admxRegBtnElementUpdate = (Get-AvaloniaHost)::FindByName($form, 'btnADMXRegElementNew')
|
|
$script:_admxRegTxtKey = (Get-AvaloniaHost)::FindByName($form, 'txtADMXRegKey')
|
|
$script:_admxRegDgElements = (Get-AvaloniaHost)::FindByName($form, 'dgADMXRegAddedElements')
|
|
|
|
# ComboBox ItemTemplate is shared across all three combos.
|
|
|
|
$script:_admxRegCbHive.ItemsSource = @(
|
|
[SettingsListItem]@{ Name = 'HKEY_LOCAL_MACHINE'; Value = 'HKLM' }
|
|
[SettingsListItem]@{ Name = 'HKEY_CURRENT_USER'; Value = 'HKCU' }
|
|
)
|
|
$hiveCurrent = [string]$RegPolicy.Hive
|
|
$hiveSel = $script:_admxRegCbHive.ItemsSource | Where-Object { $_.Value -eq $hiveCurrent } | Select-Object -First 1
|
|
if (-not $hiveSel) { $hiveSel = $script:_admxRegCbHive.ItemsSource[0] }
|
|
if ($hiveSel) { $script:_admxRegCbHive.SelectedItem = $hiveSel }
|
|
$script:_admxRegCbHive.add_SelectionChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
if ($S.SelectedItem -and $script:_admxRegCurrentPolicy) {
|
|
$script:_admxRegCurrentPolicy.Hive = [string]$S.SelectedItem.Value
|
|
}
|
|
}))
|
|
|
|
$script:_admxRegCbPolicyStatus.ItemsSource = @(
|
|
[SettingsListItem]@{ Name = 'Enabled'; Value = 'Enabled' }
|
|
[SettingsListItem]@{ Name = 'Disabled'; Value = 'Disabled' }
|
|
)
|
|
$statusCurrent = [string]$RegPolicy.PolicyStatus
|
|
$statusSel = $script:_admxRegCbPolicyStatus.ItemsSource | Where-Object { $_.Value -eq $statusCurrent } | Select-Object -First 1
|
|
if (-not $statusSel) { $statusSel = $script:_admxRegCbPolicyStatus.ItemsSource[0] }
|
|
if ($statusSel) { $script:_admxRegCbPolicyStatus.SelectedItem = $statusSel }
|
|
$script:_admxRegCbPolicyStatus.add_SelectionChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
if ($S.SelectedItem -and $script:_admxRegCurrentPolicy) {
|
|
$script:_admxRegCurrentPolicy.PolicyStatus = [string]$S.SelectedItem.Value
|
|
}
|
|
}))
|
|
|
|
# longDecimal omitted intentionally — Intune's ADMX-ingest path doesn't
|
|
# currently accept QWORD. Note carried over from the original tool.
|
|
$script:_admxRegCbDataType.ItemsSource = @(
|
|
[SettingsListItem]@{ Name = 'String'; Value = 'text' }
|
|
[SettingsListItem]@{ Name = 'Multi-string'; Value = 'multiText' }
|
|
[SettingsListItem]@{ Name = 'List'; Value = 'list' }
|
|
[SettingsListItem]@{ Name = 'DWORD (32-bit)'; Value = 'decimal' }
|
|
)
|
|
$dtCurrent = [string]$newElement.DataType
|
|
$dtSel = $script:_admxRegCbDataType.ItemsSource | Where-Object { $_.Value -eq $dtCurrent } | Select-Object -First 1
|
|
if ($dtSel) { $script:_admxRegCbDataType.SelectedItem = $dtSel }
|
|
|
|
# When the user picks a different data type, attribute control visibility
|
|
# follows. Also push the picked Value onto the live element object.
|
|
$script:_admxRegCbDataType.add_SelectionChanged((ConvertTo-AvaloniaEventScriptBlock {
|
|
param($S, $E)
|
|
if ($S.SelectedItem -and $script:_admxRegCurrentElement) {
|
|
$script:_admxRegCurrentElement.DataType = [string]$S.SelectedItem.Value
|
|
}
|
|
Set-ADMXRegAttributeControls
|
|
}))
|
|
|
|
# Apply the visibility rules for whatever DataType is currently selected.
|
|
Set-ADMXRegAttributeControls
|
|
|
|
# Double-click on an element row swaps the current PolicyElement for the
|
|
# selected one, switching the form from add-mode to update-mode.
|
|
$script:_admxRegDgElements.add_DoubleTapped((ConvertTo-AvaloniaEventScriptBlock {
|
|
if (-not $script:_admxRegDgElements.SelectedItem) { return }
|
|
$script:_admxRegBtnElementAdd.IsVisible = $false
|
|
$script:_admxRegBtnElementUpdate.IsVisible = $true
|
|
|
|
$selected = $script:_admxRegDgElements.SelectedItem
|
|
$script:_admxRegCurrentElement = $selected
|
|
if ($script:_admxRegGrdElement) { $script:_admxRegGrdElement.DataContext = $selected }
|
|
|
|
# Re-sync the DataType ComboBox with the swapped-in element.
|
|
$dtCurrent = [string]$selected.DataType
|
|
$dtSel = $script:_admxRegCbDataType.ItemsSource | Where-Object { $_.Value -eq $dtCurrent } | Select-Object -First 1
|
|
if ($dtSel) { $script:_admxRegCbDataType.SelectedItem = $dtSel }
|
|
|
|
Set-ADMXRegAttributeControls
|
|
}))
|
|
|
|
$ui.AddXamlEvent($form, 'btnADMXRegElementAdd', 'add_Click', ({
|
|
if ($script:_admxRegTxtElementKey.Text -and (Get-ADMXRegIsKeySupported $script:_admxRegTxtElementKey.Text) -eq $false) { return }
|
|
if (-not $script:_admxRegCurrentPolicy.Key) {
|
|
$ui.ShowMessageBox('The Key value must be specified for the policy', 'Reg Values', 'OK', 'Error')
|
|
return
|
|
}
|
|
if ($script:_admxRegTxtSeparator) {
|
|
$script:_admxRegCurrentElement.AttributeSeparator = $script:_admxRegTxtSeparator.Text
|
|
}
|
|
$script:_admxRegCurrentPolicy.PolicyElements.Add($script:_admxRegCurrentElement)
|
|
$script:_admxRegCurrentElement = [ADMXRegPolicyElement]::new()
|
|
if ($script:_admxRegGrdElement) { $script:_admxRegGrdElement.DataContext = $script:_admxRegCurrentElement }
|
|
|
|
# Reset the data-type ComboBox to no selection so the next row starts
|
|
# clean. Visibility helpers re-run via SelectionChanged.
|
|
$script:_admxRegCbDataType.SelectedItem = $null
|
|
}))
|
|
|
|
$ui.AddXamlEvent($form, 'btnADMXRegElementNew', 'add_Click', ({
|
|
$script:_admxRegCurrentElement = [ADMXRegPolicyElement]::new()
|
|
if ($script:_admxRegGrdElement) { $script:_admxRegGrdElement.DataContext = $script:_admxRegCurrentElement }
|
|
$script:_admxRegBtnElementAdd.IsVisible = $true
|
|
$script:_admxRegBtnElementUpdate.IsVisible = $false
|
|
$script:_admxRegCbDataType.SelectedItem = $null
|
|
}))
|
|
|
|
$ui.AddXamlEvent($form, 'btnADMXRegAddNew', 'add_Click', ({
|
|
if ((Get-ADMXRegIsKeySupported $script:_admxRegTxtKey.Text) -eq $false) { return }
|
|
if ($script:_admxRegIsNewPolicy) {
|
|
$script:_admxRegValuesPanel.DataContext.ADMXPolicies.Add($script:_admxRegCurrentPolicy)
|
|
}
|
|
Show-ModalObject
|
|
}))
|
|
|
|
$ui.AddXamlEvent($form, 'btnADMXRegCancel', 'add_Click', ({
|
|
$script:_admxRegPoliciesForm = $null
|
|
Show-ModalObject
|
|
}))
|
|
|
|
$ui.ShowModalForm('Add new reg policy', $form, $true)
|
|
}
|
|
|
|
function Set-ADMXRegAttributeControls
|
|
{
|
|
# Show or hide the per-data-type attribute controls so users only see the
|
|
# toggles that are relevant for the picked DataType. WPF Visibility =
|
|
# "Collapsed" -> Avalonia IsVisible = $false.
|
|
if (-not $script:_admxRegCbDataType -or -not $script:_admxRegCbDataType.SelectedItem) {
|
|
# No selection yet — hide all conditional controls.
|
|
if ($script:_admxRegSpSeparator) { $script:_admxRegSpSeparator.IsVisible = $false }
|
|
if ($script:_admxRegTxtSeparator) { $script:_admxRegTxtSeparator.IsVisible = $false }
|
|
if ($script:_admxRegSpValuePrefix) { $script:_admxRegSpValuePrefix.IsVisible = $false }
|
|
if ($script:_admxRegTxtValuePrefix){ $script:_admxRegTxtValuePrefix.IsVisible= $false }
|
|
if ($script:_admxRegSpExpandable) { $script:_admxRegSpExpandable.IsVisible = $false }
|
|
if ($script:_admxRegChkExpandable) { $script:_admxRegChkExpandable.IsVisible = $false }
|
|
if ($script:_admxRegSpAdditive) { $script:_admxRegSpAdditive.IsVisible = $false }
|
|
if ($script:_admxRegChkAdditive) { $script:_admxRegChkAdditive.IsVisible = $false }
|
|
if ($script:_admxRegTxtElementValName) { $script:_admxRegTxtElementValName.IsReadOnly = $false }
|
|
return
|
|
}
|
|
|
|
$dt = [string]$script:_admxRegCbDataType.SelectedItem.Value
|
|
|
|
$showSep = ($dt -eq 'list' -or $dt -eq 'multiText')
|
|
$script:_admxRegSpSeparator.IsVisible = $showSep
|
|
$script:_admxRegTxtSeparator.IsVisible = $showSep
|
|
if ($showSep -and [string]::IsNullOrEmpty($script:_admxRegTxtSeparator.Text)) {
|
|
$script:_admxRegTxtSeparator.Text = ';'
|
|
}
|
|
|
|
$showPrefix = ($dt -eq 'list')
|
|
$script:_admxRegSpValuePrefix.IsVisible = $showPrefix
|
|
$script:_admxRegTxtValuePrefix.IsVisible = $showPrefix
|
|
|
|
$script:_admxRegTxtElementValName.IsReadOnly = ($dt -eq 'list')
|
|
|
|
$showExpandable = ($dt -eq 'text' -or $dt -eq 'list')
|
|
$script:_admxRegSpExpandable.IsVisible = $showExpandable
|
|
$script:_admxRegChkExpandable.IsVisible = $showExpandable
|
|
|
|
$showAdditive = ($dt -eq 'list')
|
|
$script:_admxRegSpAdditive.IsVisible = $showAdditive
|
|
$script:_admxRegChkAdditive.IsVisible = $showAdditive
|
|
}
|
|
|
|
# ─── Reg Values — Intune ingestion (Slice 5b2) ────────────────────────────────
|
|
#
|
|
# Verbatim port of UI/WPF/Extensions/IntuneToolsUI.ps1 Import-ADMXRegProfileToIntune.
|
|
# No UI deps beyond $script:_admxRegValuesPanel.DataContext; everything else is
|
|
# data-shape (XML clone + OMA-URI assembly) and Graph POST.
|
|
|
|
function Import-ADMXRegProfileToIntune
|
|
{
|
|
$ui = $script:UIProvider
|
|
$regProfile = $script:_admxRegValuesPanel.DataContext
|
|
if (-not $regProfile -or -not $regProfile.ProfileName) {
|
|
$ui.ShowMessageBox('Custom Profile Name must be specified', 'Reg Values', 'OK', 'Error')
|
|
return
|
|
}
|
|
if ($regProfile.ADMXPolicies.Count -eq 0) {
|
|
$ui.ShowMessageBox('Add at least one registry policy before importing', 'Reg Values', 'OK', 'Information')
|
|
return
|
|
}
|
|
|
|
[xml]$xml = $script:_admxRegTemplate
|
|
|
|
# The synthesized category needs a unique name so re-imports under the same
|
|
# ingest namespace don't collide.
|
|
$guidId = [Guid]::NewGuid().Guid
|
|
$regPolicyFileName = "RegPolicy_$guidId"
|
|
$xml.policyDefinitions.categories.category.name = ($xml.policyDefinitions.categories.category.name + '_' + $guidId)
|
|
|
|
$intuneObj = [PSCustomObject]@{
|
|
'@odata.type' = '#microsoft.graph.windows10CustomConfiguration'
|
|
displayName = $regProfile.ProfileName
|
|
omaSettings = @()
|
|
roleScopeTagIds = @()
|
|
assignments = @()
|
|
}
|
|
if ($regProfile.ProfileDescription) {
|
|
$intuneObj | Add-Member -MemberType NoteProperty -Name description -Value $regProfile.ProfileDescription
|
|
}
|
|
|
|
$admxRegSettings = @()
|
|
|
|
foreach ($regPolicy in $regProfile.ADMXPolicies)
|
|
{
|
|
$omaUriString = if ($regPolicy.PolicyStatus -eq 'Enabled') { '<enabled/>' } else { '<disabled/>' }
|
|
|
|
if ($regPolicy.PolicyName) {
|
|
$policyName = $regPolicy.PolicyName -replace ' ', '_'
|
|
}
|
|
else {
|
|
$policyName = [Guid]::NewGuid().Guid
|
|
}
|
|
|
|
# Clone the seed <policy> for each user-defined policy. We mutate the
|
|
# clone, then append; the original is removed once the loop completes.
|
|
$newNode = $xml.policyDefinitions.policies.ChildNodes[0].CloneNode($true)
|
|
$newNode.name = $policyName
|
|
$newNode.class = if ($regPolicy.Hive -eq 'HKLM') { 'Machine' } else { 'User' }
|
|
$newNode.displayName = "`$(string.$policyName)"
|
|
$newNode.presentation = "`$(presentation.$policyName)"
|
|
$newNode.key = $regPolicy.Key.Trim('\')
|
|
$newNode.parentCategory.ref = $xml.policyDefinitions.categories.category.name
|
|
|
|
if (-not $regPolicy.StatusValueName) {
|
|
[void]$newNode.RemoveChild($newNode.SelectSingleNode('enabledValue'))
|
|
[void]$newNode.RemoveChild($newNode.SelectSingleNode('disabledValue'))
|
|
}
|
|
else {
|
|
$newNode.valueName = $regPolicy.StatusValueName
|
|
}
|
|
|
|
$omaUriItems = @()
|
|
if ($null -eq $regPolicy.PolicyElements -or $regPolicy.PolicyElements.Count -eq 0) {
|
|
[void]$newNode.RemoveChild($newNode.SelectSingleNode('elements'))
|
|
}
|
|
else
|
|
{
|
|
foreach ($element in $regPolicy.PolicyElements)
|
|
{
|
|
$child = $xml.CreateElement($element.DataType)
|
|
$elementId = $null
|
|
|
|
if ($element.DataType -in @('multiText','list')) {
|
|
# User-set separator is stored per element; fall back to
|
|
# ';' for legacy in-memory objects.
|
|
$splitter = ?? $element.AttributeSeparator ';'
|
|
$value = $element.Value -replace $splitter, ''
|
|
}
|
|
else {
|
|
$value = $element.Value
|
|
}
|
|
|
|
# valueName goes on every element type except "list" (which is
|
|
# keyed by an Id derived from the policy/element key path).
|
|
if ($element.DataType -ne 'list') {
|
|
Add-ADMXRegXmlAttribute $child 'valueName' $element.ValueName
|
|
}
|
|
else {
|
|
if ($element.AttributePrefix) {
|
|
Add-ADMXRegXmlAttribute $child 'valuePrefix' $element.AttributePrefix
|
|
}
|
|
}
|
|
|
|
if (($element.DataType -eq 'text' -or $element.DataType -eq 'list') -and $element.AttributeExpandable) {
|
|
Add-ADMXRegXmlAttribute $child 'expandable' 'true'
|
|
}
|
|
if ($element.DataType -eq 'list' -and $element.AttributeAdditive) {
|
|
Add-ADMXRegXmlAttribute $child 'additive' 'true'
|
|
}
|
|
if ($element.AttributeSoft) {
|
|
Add-ADMXRegXmlAttribute $child 'soft' 'true'
|
|
}
|
|
|
|
if ($element.DataType -eq 'list') {
|
|
$keyStr = ?? $element.Key $regPolicy.Key
|
|
if ($keyStr) { $idStr = $keyStr.Trim('\').Split('\')[-1] }
|
|
else { $idStr = [Guid]::NewGuid().Guid }
|
|
$elementId = $idStr + '_Id'
|
|
Add-ADMXRegXmlAttribute $child 'id' $elementId
|
|
}
|
|
else {
|
|
$elementId = $element.ValueName + '_Id'
|
|
Add-ADMXRegXmlAttribute $child 'id' $elementId
|
|
}
|
|
|
|
if ($element.Key) {
|
|
Add-ADMXRegXmlAttribute $child 'key' $element.Key.Trim('\')
|
|
}
|
|
|
|
$escapedValue = [System.Security.SecurityElement]::Escape($value) -replace '&#xF000;', ''
|
|
$omaUriItems += "<data id=`"$elementId`" value=`"$escapedValue`"/>"
|
|
|
|
[void]$newNode.SelectSingleNode('elements').AppendChild($child)
|
|
}
|
|
}
|
|
|
|
if ($omaUriItems.Count -gt 0) {
|
|
$omaUriString = $omaUriString + [Environment]::NewLine + [Environment]::NewLine + ($omaUriItems -join [Environment]::NewLine)
|
|
}
|
|
if ((Get-SettingValue 'FormatOMAURI') -eq $true) {
|
|
$omaUriString = Update-XmlFormatting $omaUriString
|
|
}
|
|
|
|
[void]$xml.policyDefinitions.SelectSingleNode('policies').AppendChild($newNode)
|
|
|
|
$direction = if ($regPolicy.Hive -eq 'HKLM') { 'Device' } else { 'User' }
|
|
$admxRegSettings += [PSCustomObject]@{
|
|
'@odata.type' = '#microsoft.graph.omaSettingString'
|
|
displayName = "Set $policyName"
|
|
omaUri = "./$direction/Vendor/MSFT/Policy/Config/IntuneManagementReg~$($regProfile.PolicyType)~$($newNode.parentCategory.ref)/$policyName"
|
|
value = $omaUriString
|
|
}
|
|
}
|
|
|
|
# Remove the seed/template policy before serialisation.
|
|
[void]$xml.policyDefinitions.SelectSingleNode('policies').RemoveChild(
|
|
$xml.policyDefinitions.policies.SelectSingleNode('policy'))
|
|
|
|
$xmlString = Format-XML $xml
|
|
if ((Get-SettingValue 'FormatOMAURI') -eq $true) {
|
|
$xmlString = Update-XmlFormatting $xmlString
|
|
}
|
|
|
|
$intuneObj.omaSettings += [PSCustomObject]@{
|
|
'@odata.type' = '#microsoft.graph.omaSettingString'
|
|
displayName = 'Reg ADMX Ingestion'
|
|
description = 'This XML is generated by Intune Management tool'
|
|
omaUri = "./Device/Vendor/MSFT/Policy/ConfigOperations/ADMXInstall/IntuneManagementReg/$($regProfile.PolicyType)/$regPolicyFileName"
|
|
value = $xmlString
|
|
}
|
|
$intuneObj.omaSettings += $admxRegSettings
|
|
|
|
$json = $intuneObj | ConvertTo-Json -Depth 20
|
|
|
|
$tokenId = Get-DefaultTokenId
|
|
$resp = Invoke-MSGraphAPI -Url 'deviceManagement/deviceConfigurations' -Content $json -HttpMethod 'POST' -TokenId $tokenId -FullResponseObject
|
|
|
|
if ($resp -and $resp.Success) {
|
|
Write-Log "Custom profile '$($intuneObj.displayName)' created with id $($resp.Content.Id)"
|
|
$ui.ShowMessageBox("Profile '$($intuneObj.displayName)' created successfully.", 'Reg Values', 'OK', 'Information')
|
|
}
|
|
else {
|
|
$errInfo = if ($resp) { "$($resp.StatusCode) $($resp.StatusDescription)" } else { 'no response' }
|
|
Write-Log "Failed to create device configuration profile '$($intuneObj.displayName)' - $errInfo" 3
|
|
$ui.ShowMessageBox("Failed to create device configuration profile '$($intuneObj.displayName)'.`n`n$errInfo`n`nCheck the log for details.", 'Reg Values', 'OK', 'Error')
|
|
}
|
|
}
|