IntuneManagement 4.0.0-beta1

This commit is contained in:
Mikael Karlsson
2026-09-23 19:13:09 +10:00
commit 7869619510
892 changed files with 577109 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

@@ -0,0 +1,20 @@
# WPF UI extension for the Atlassian documentation output provider.
#
# Attaches file-browse metadata onto the registered [DocumentationRegistry]
# entry so the bulk-doc form's browse-button-wiring loop can build a WinForms
# SaveFileDialog handler. Format matches Add-WpfDocumentationFileBrowse
# (Button/TextBox/Save/Filter). The provider file itself
# (Internal/Documentation/OutputProviders/DocumentationOutputAtlassian.ps1)
# stays free of XAML control names and Windows-Forms filter strings — only the
# active UI backend's ClassExtensions load per session, so the Avalonia
# counterpart won't conflict.
#
# Unlike HTML there is no CSS row: Confluence storage format carries no
# stylesheet, so the panel has a single browse control.
$provider = [DocumentationRegistry]::FindOutput('atlassian')
if ($provider) {
Add-Member -InputObject $provider -MemberType NoteProperty -Force -Name FileBrowseControls -Value @(
@{ Button='browseAtlassianDocumentName'; TextBox='txtAtlassianDocumentName'; Save=$true; Filter='Confluence storage format (*.html)|*.html|All files (*.*)|*.*' }
)
}
@@ -0,0 +1,18 @@
# WPF UI extension for the HTML documentation output provider.
#
# Attaches file-browse metadata onto the registered [DocumentationRegistry]
# entry so the bulk-doc form's browse-button-wiring loop can build WinForms
# SaveFileDialog / OpenFileDialog handlers. Format matches Add-WpfDocumentationFileBrowse
# (Button/TextBox/Save/Filter). The provider file itself
# (Internal/Documentation/OutputProviders/DocumentationOutputHTML.ps1) stays
# free of XAML control names and Windows-Forms filter strings — only the
# active UI backend's ClassExtensions load per session, so the Avalonia
# counterpart won't conflict.
$provider = [DocumentationRegistry]::FindOutput('html')
if ($provider) {
Add-Member -InputObject $provider -MemberType NoteProperty -Force -Name FileBrowseControls -Value @(
@{ Button='browseHTMLDocumentName'; TextBox='txtHTMLDocumentName'; Save=$true; Filter='HTML files (*.html)|*.html|All files (*.*)|*.*' }
@{ Button='browseHTMLCSSFile'; TextBox='txtHTMLCSSFile'; Save=$false; Filter='CSS files (*.css)|*.css|All files (*.*)|*.*' }
)
}
@@ -0,0 +1,10 @@
# WPF UI extension for the Markdown documentation output provider.
# See DocumentationOutputHTMLUIExtension.ps1 for the rationale + pattern.
$provider = [DocumentationRegistry]::FindOutput('md')
if ($provider) {
Add-Member -InputObject $provider -MemberType NoteProperty -Force -Name FileBrowseControls -Value @(
@{ Button='browseMDDocumentName'; TextBox='txtMDDocumentName'; Save=$true; Filter='Markdown files (*.md)|*.md|All files (*.*)|*.*' }
@{ Button='browseMDCSSFile'; TextBox='txtMDCSSFile'; Save=$false; Filter='CSS files (*.css)|*.css|All files (*.*)|*.*' }
)
}
@@ -0,0 +1,14 @@
# WPF UI extension for the Word documentation output provider.
# See DocumentationOutputHTMLUIExtension.ps1 for the rationale + pattern.
#
# Note: WordDocumentName's original filter included a PDF entry because Word
# can save PDFs directly. Preserved here for parity with the pre-refactor
# Add-WpfDocumentationFileBrowse call.
$provider = [DocumentationRegistry]::FindOutput('word')
if ($provider) {
Add-Member -InputObject $provider -MemberType NoteProperty -Force -Name FileBrowseControls -Value @(
@{ Button='browseWordDocumentName'; TextBox='txtWordDocumentName'; Save=$true; Filter='Word files (*.docx)|*.docx|PDF files (*.pdf)|*.pdf|All files (*.*)|*.*' }
@{ Button='browseWordDocumentTemplate'; TextBox='txtWordDocumentTemplate'; Save=$false; Filter='Word templates (*.dotx;*.dotm)|*.dotx;*.dotm|All files (*.*)|*.*' }
)
}
@@ -0,0 +1,187 @@
##############################################
# ScriptBlocks
##############################################
$SBAddApplicationUIExportExtension = [scriptblock] {
param($Form)
if (-not $Form) { return }
Add-UIScriptExportExtensions $Form
$grdExportProperties = $null
try {
$grdExportProperties = $Form.FindName("grdExportProperties")
}
catch {}
if (-not $grdExportProperties) { return }
if ($grdExportProperties.FindName("chkExportAppContent")) {
return # Already added
}
$xaml = @"
<StackPanel $($script:wpfNS) Orientation="Horizontal" Margin="0,0,5,0">
<Label Content="Export application file" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Export the application file. Note: Application file will only be exported if ecryption file is found." />
</StackPanel>
"@
try {
$label = [Windows.Markup.XamlReader]::Parse($xaml)
$chkExportAppContent = [System.Windows.Controls.CheckBox]::new()
$chkExportAppContent.IsChecked = ((Get-SettingStoreValue "Intune" "ExportAppFile" "false") -eq "true")
$chkExportAppContent.VerticalAlignment = "Center"
$chkExportAppContent.Name = "chkExportAppContent"
Set-CacheObject "ExportAppContent" $chkExportAppContent.IsChecked
$rd = [System.Windows.Controls.RowDefinition]::new()
$rd.Height = [double]::NaN
$grdExportProperties.RowDefinitions.Add($rd)
$label.SetValue([System.Windows.Controls.Grid]::RowProperty, $grdExportProperties.RowDefinitions.Count - 1)
$chkExportAppContent.SetValue([System.Windows.Controls.Grid]::RowProperty, $grdExportProperties.RowDefinitions.Count - 1)
$chkExportAppContent.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 1)
$grdExportProperties.Children.Add($label)
$grdExportProperties.Children.Add($chkExportAppContent)
$Form.RegisterName($chkExportAppContent.Name, $chkExportAppContent)
$changed = [scriptblock] {
param($S, $E)
Set-CacheObject "ExportAppContent" $this.IsChecked
}
$chkExportAppContent.Add_Checked($changed)
$chkExportAppContent.Add_Unchecked($changed)
}
catch {}
}
$SBAddApplicationUIDetailsExtension = [scriptblock] {
param($Form)
$buttonPanel = $Form.FindName("pnlButtons")
if (-not $buttonPanel) { return }
$btnUpload = New-Object System.Windows.Controls.Button
$btnUpload.Content = 'Upload'
$btnUpload.Name = 'btnUploadAppfile'
$btnUpload.Margin = "0,0,5,0"
$btnUpload.Width = "100"
$btnUpload.Add_Click({
if ($script:dgIntuneManagerObjects.SelectedItem.Object.publishingState -ne "notPublished") {
# Only allow upload of not published apps
# Use portal to replace app file...
if (([System.Windows.MessageBox]::Show("Are you sure you want to upload a new file for the app?`n`nApplication:`n$($script:dgIntuneManagerObjects.SelectedItem.Name)", "Update app file?", "YesNo", "Warning")) -ne "Yes") {
return
}
}
$pkgPath = Get-SettingValue "IntuneAppPackagesFolder"
$of = [System.Windows.Forms.OpenFileDialog]::new()
$of.FileName = $script:dgIntuneManagerObjects.SelectedItem.Object.fileName
$of.DefaultExt = "*.intunewin"
$of.Filter = "Intune Win32 (*.intunewin)|*.*"
$of.Multiselect = $false
if ($pkgPath -and [IO.Directory]::Exists($pkgPath)) {
$of.InitialDirectory = $pkgPath
}
if ($of.ShowDialog() -eq "OK") {
Write-Status "Import $($script:dgIntuneManagerObjects.SelectedItem.Object.displayName) file"
Start-ApplicationImportFile $script:dgIntuneManagerObjects.SelectedItem $of.FileName
Write-Status ""
}
})
$buttonPanel.Children.Insert(0, $btnUpload)
$btnDownload = New-Object System.Windows.Controls.Button
$btnDownload.Content = 'Download'
$btnDownload.Name = 'btnDownloadAppfile'
$btnDownload.Margin = "0,0,5,0"
$btnDownload.Width = "100"
$btnDownload.Add_Click({
Write-Status "Download file"
$obj = $script:dgIntuneManagerObjects.SelectedItem.Object
#$obj = Invoke-MSGraphAPI -Url "/deviceAppManagement/mobileApps/$($obj.id)"
$pkgPath = Get-SettingValue "IntuneAppDownloadFolder" (Get-SettingValue "IntuneAppPackagesFolder")
$dlgSave = [System.Windows.Forms.SaveFileDialog]::new()
$dlgSave.InitialDirectory = $pkgPath
$dlgSave.FileName = ($obj.FileName + ".encrypted")
$dlgSave.DefaultExt = "*.encrypted"
$dlgSave.Filter = "Encrypted intunewin (*.encrypted)|*.encrypted|All files (*.*)|*.*"
if ($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) {
$contentFileObj = Start-DownloadAppContent $script:dgIntuneManagerObjects.SelectedItem $dlgSave.FileName
if ([IO.File]::Exists($dlgSave.FileName)) {
$fullPath = Find-AppEncryptionFile $obj $contentFileObj $pkgPath
if ([IO.File]::Exists($fullPath) -eq $false) {
if (([System.Windows.MessageBox]::Show("Could not find decryption file for $($obj.displayName)`nApp Id: $($obj.id)`nContent version $($obj.committedContentVersion)`n`nDo you want to browse for the file?", "Encryption file not found", "YesNo", "Warning")) -eq "Yes") {
$of = [System.Windows.Forms.OpenFileDialog]::new()
$of.InitialDirectory = $pkgPath
$of.DefaultExt = "*.json"
$of.Filter = "Json (*.json)|*.json"
$of.Multiselect = $false
if ($of.ShowDialog() -eq "OK") {
$fullPath = $of.FileName
}
}
}
if ([IO.File]::Exists($fullPath)) {
Write-Status "Decrypting file"
$encryptionInfo = ConvertFrom-Json ([IO.File]::ReadAllText($fullPath))
if ($encryptionInfo.fileEncryptionInfo) {
$encryptionInfo = $encryptionInfo.fileEncryptionInfo
}
$destination = $pkgPath + ("\$($obj.FileName)" -replace 'intunewin$', 'zip')
Start-DecryptFile $dlgSave.Filename $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector
try { [IO.File]::Delete($dlgSave.Filename) }
catch {
Write-LogError "Failed to delete exported encrypted file" $_.Exception
}
}
else {
Write-Log "Decryption file for $($obj.displayName) not found. Skipping decryption" 2
}
}
}
Write-Status ""
})
$buttonPanel.Children.Insert(0, $btnDownload)
}
##############################################
# Functions
##############################################
function Invoke-InitializeApplicationUIExtensions {
$typeClasses = @()
$typeClasses += Get-SingletonObject "ApplicationType"
foreach ($typeClass in $typeClasses) {
Add-ObjectMethod $typeClass "AddUIExportExtensions" $SBAddApplicationUIExportExtension
Add-ObjectMethod $typeClass "AddUIDetailsExtension" $SBAddApplicationUIDetailsExtension
}
}
##############################################
# Initialize
##############################################
Invoke-InitializeApplicationUIExtensions
@@ -0,0 +1,69 @@
##############################################
# ScriptBlocks
##############################################
$SBAddCommonUILoadImage = [scriptblock] {
try {
$this.IconImage = $this.GetImage()
}
catch {}
}
# Capture the module-bound Get-XamlObject + UI root path at attachment time.
# Per [[scriptmethod-loses-module-scope]], Add-Member ScriptMethod scriptblocks
# can't reliably resolve module-scoped vars/functions by name. Routing through
# $script:UIProvider here also fails because the methods are attached at module
# load, *before* the loader instantiates the provider.
#
# Only the methods are attached here. The icons themselves are parsed later, by
# Initialize-MenuItemIcons (Extensions/UIMenuIcons*.ps1) when a menu is built -
# after the splash is up, not on the Import-Module critical path.
$_getXamlObjectFn = ${function:Get-XamlObject}
$_uiRootFolder = $script:AppUIRootFolder
$SBAddCommonUIGetImage = [scriptblock] {
$fi = [IO.FileInfo]($_uiRootFolder + "\Xaml\Icons\$($this.Icon).xaml")
if($fi.Exists) {
return (& $_getXamlObjectFn ($_uiRootFolder + "\Xaml\Icons\$($this.Icon).xaml"))
}
return $null
}.GetNewClosure()
##############################################
# Functions
##############################################
function Invoke-InitializeCommonUIExtensions {
$typeClasses = @()
$groupClasses = @()
Get-SubClasses "IntunePolicyTypeBase" | ForEach-Object {
if(Test-ClassIsAbstract $_) { return }
$policyTypeName = $_.Name
try {
$tmpClass = Get-SingletonObject $_.Name
if ($null -ne $tmpClass.PolicyGroup) {
$typeClasses += $tmpClass
}
}
catch {
Write-LogError "Failed to initialize class $($policyTypeName)." $_.Exception
}
}
Get-SubClasses "IntunePolicyGroupBase" | ForEach-Object {
$groupClasses += Get-SingletonObject $_.Name
}
foreach ($class in ($typeClasses + $groupClasses)) {
Add-ObjectMethod $class "LoadIconImage" $SBAddCommonUILoadImage
Add-ObjectMethod $class "GetImage" $SBAddCommonUIGetImage
}
}
##############################################
# Initialize
##############################################
Invoke-InitializeCommonUIExtensions
@@ -0,0 +1,243 @@
##############################################
# ScriptBlocks
##############################################
$SBAddPolicyFileUIExportExtension = [scriptblock] {
param($Form)
if (-not $Form) { return }
Add-UIPolicyFileExportExtensions $Form
}
$SBAddPolicyFileUIDetailsExtension = [scriptblock] {
param($Form)
$buttonPanel = $Form.FindName("pnlButtons")
if (-not $buttonPanel) { return }
$btn = New-Object System.Windows.Controls.Button
$btn.Content = 'Download'
$btn.Name = 'btnDownload'
$btn.Margin = "0,0,5,0"
$btn.Width = "100"
$btn.Add_Click({
Invoke-DownloadPolicyFile $script:dgIntuneManagerObjects.SelectedItem
})
$buttonPanel.Children.Insert(0, $btn)
$btn = New-Object System.Windows.Controls.Button
$btn.Content = 'Edit'
$btn.Name = 'btnEdit'
$btn.Margin = "0,0,5,0"
$btn.Width = "100"
$btn.Add_Click({
Invoke-EditPolicyFile $script:dgIntuneManagerObjects.SelectedItem
})
$buttonPanel.Children.Insert(1, $btn)
}
##############################################
# Functions
##############################################
function Invoke-InitializePolicyFileUIExtensions {
$typeClasses = @()
Get-SubClasses "IntunePolicyTypeBase" | ForEach-Object {
if(Test-ClassIsAbstract $_) { return }
try {
$tmpClass = Get-SingletonObject $_.Name
if ($null -ne $tmpClass.PolicyGroup -and $tmpClass._PolicyFileAttributes.Count -gt 0) {
$typeClasses += $tmpClass
}
}
catch {}
}
foreach ($typeClass in $typeClasses) {
Add-ObjectMethod $typeClass "AddUIExportExtensions" $SBAddPolicyFileUIExportExtension
Add-ObjectMethod $typeClass "AddUIDetailsExtension" $SBAddPolicyFileUIDetailsExtension
}
}
function Add-UIPolicyFileExportExtensions {
param(
$Form,
$Description = "Export the files associated with selected profiles"
)
$grdExportProperties = $null
try {
$grdExportProperties = $Form.FindName("grdExportProperties")
}
catch {}
if (-not $grdExportProperties) { return }
if ($grdExportProperties.FindName("chkExportPolicyFile")) {
return # Already added
}
$xaml = @"
<StackPanel $($script:wpfNS) Orientation="Horizontal" Margin="0,0,5,0">
<Label Content="Export file" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="$Description" />
</StackPanel>
"@
try {
$label = [Windows.Markup.XamlReader]::Parse($xaml)
$chkExportPolicyFiles = [System.Windows.Controls.CheckBox]::new()
$chkExportPolicyFiles.IsChecked = $true
$chkExportPolicyFiles.VerticalAlignment = "Center"
$chkExportPolicyFiles.Name = "chkExportPolicyFiles"
Set-CacheObject "ExportPolicyFiles" $chkExportPolicyFiles.IsChecked
$rd = [System.Windows.Controls.RowDefinition]::new()
$rd.Height = [double]::NaN
$grdExportProperties.RowDefinitions.Add($rd)
$label.SetValue([System.Windows.Controls.Grid]::RowProperty, $grdExportProperties.RowDefinitions.Count - 1)
$chkExportPolicyFiles.SetValue([System.Windows.Controls.Grid]::RowProperty, $grdExportProperties.RowDefinitions.Count - 1)
$chkExportPolicyFiles.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 1)
$grdExportProperties.Children.Add($label)
$grdExportProperties.Children.Add($chkExportPolicyFiles)
$Form.RegisterName($chkExportPolicyFiles.Name, $chkExportPolicyFiles)
$changed = [scriptblock] {
param($S, $E)
Set-CacheObject "ExportPolicyFiles" $this.IsChecked
}
$chkExportPolicyFiles.Add_Checked($changed)
$chkExportPolicyFiles.Add_Unchecked($changed)
}
catch {}
}
function Invoke-EditPolicyFile {
param($Policy)
if (-not $Policy) { return }
if ($Policy.IsFullObject -eq $false) {
[void]$Policy.Get()
}
$attribute = $Policy.PolicyType._PolicyFileAttributes[0] # TODO: Support multiple attributes
if (-not $Policy.JsonObject.$attribute) { return }
$script:currentPolicyFileObject = $Policy
$script:editForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\EditScriptDialog.xaml"))
if (-not $script:editForm) { return }
$script:UIProvider.SetXamlProperty($script:editForm, "txtEditScriptTitle", "Text", "Edit: $($Policy.JsonObject.displayName)")
$policyFileText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Policy.JsonObject.$attribute))
$script:UIProvider.SetXamlProperty($script:editForm, "txtScriptText", "Text", $policyFileText)
$script:currentModal = $null
if ($script:grdModal.Children.Count -gt 0) {
$script:currentModal = $script:grdModal.Children[0]
}
$script:UIProvider.AddXamlEvent($script:editForm, "btnSaveScriptEdit", "add_click", {
$policyFileText = $script:UIProvider.GetXamlProperty($script:editForm, "txtScriptText", "Text")
$pre = [System.Text.Encoding]::UTF8.GetPreamble()
$utfBOM = [System.Text.Encoding]::UTF8.GetString($pre)
if ($policyFileText.startsWith($utfBOM)) {
# Remove UTF8 BOM bytes
$policyFileText = $policyFileText.Remove(0, $utfBOM.Length)
}
$bytes = [System.Text.Encoding]::UTF8.GetBytes($policyFileText)
$encodedText = [Convert]::ToBase64String($bytes)
$attribute = $script:currentPolicyFileObject.PolicyType._PolicyFileAttributes[0]
if ($script:currentPolicyFileObject.JsonObject.$attribute -ne $encodedText) {
# Save script
if (($script:UIProvider.ShowMessageBox("Are you sure you want to update the file?`n`nObject:`n$($script:currentPolicyFileObject.Name)", "Update file?", "YesNo", "Warning")) -eq "Yes") {
Write-Status "Update $($script:currentPolicyFileObject.Name)"
$clonedObject = $script:currentPolicyFileObject.Clone()
$clonedObject.JsonObject.$attribute = $encodedText
Remove-GraphPropertiesForImport $clonedObject $clonedObject.JsonObject
foreach ($prop in $script:currentPolicyFileObject.PolicyType.PropertiesToRemoveForUpdate) {
Remove-Property $clonedObject.JsonObject $prop
}
Remove-Property $clonedObject.JsonObject "Assignments"
Remove-Property $clonedObject.JsonObject "isAssigned"
if ($script:currentPolicyFileObject.Set($null, $clonedObject.JsonString, "PATCH", $null)) {
Write-Log "File saved successfully"
}
else {
Write-Log "Failed to save script" 3
}
Write-Status ""
}
}
$script:grdModal.Children.Clear()
if ($script:currentModal) {
$script:grdModal.Children.Add($script:currentModal)
}
[System.Windows.Forms.Application]::DoEvents()
})
$script:UIProvider.AddXamlEvent($script:editForm, "btnCancelScriptEdit", "add_click", {
$script:grdModal.Children.Clear()
if ($script:currentModal) {
$script:grdModal.Children.Add($script:currentModal)
}
[System.Windows.Forms.Application]::DoEvents()
})
$script:grdModal.Children.Clear()
$script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty, 1)
$script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 1)
$script:grdModal.Children.Add($script:editForm) | Out-Null
[System.Windows.Forms.Application]::DoEvents()
}
function Invoke-DownloadPolicyFile
{
param($Policy)
if(-not $Policy) { return }
if($Policy.IsFullObject -eq $false) {
[void]$Policy.Get()
}
Write-Status ""
$attribute = $Policy.PolicyType._PolicyFileAttributes[0]
if($Policy.JsonObject.$attribute)
{
$fileName = ?? $Policy.JsonObject.FileName "$($attribute).json"
Write-Log "Download PowerShell file '$($fileName)' from $($Policy.Name)"
$dlgSave = [System.Windows.Forms.SaveFileDialog]::new()
$dlgSave.InitialDirectory = Get-SettingValue "RootFolder" $env:Temp
$dlgSave.FileName = $fileName
if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename)
{
# Changed to WriteAllBytes to get rid of BOM characters from Custom Attribute file
[IO.File]::WriteAllBytes($dlgSave.FileName, ([System.Convert]::FromBase64String($Policy.JsonObject.$attribute)))
}
}
}
##############################################
# Initialize
##############################################
Invoke-InitializePolicyFileUIExtensions
@@ -0,0 +1,232 @@
##############################################
# ScriptBlocks
##############################################
$SBAddScriptUIExportExtension = [scriptblock] {
param($Form)
if (-not $Form) { return }
Add-UIScriptExportExtensions $Form
}
$SBAddScriptUIDetailsExtension = [scriptblock] {
param($Form)
$buttonPanel = $Form.FindName("pnlButtons")
if (-not $buttonPanel) { return }
$btn = New-Object System.Windows.Controls.Button
$btn.Content = 'Download'
$btn.Name = 'btnDownload'
$btn.Margin = "0,0,5,0"
$btn.Width = "100"
$btn.Add_Click({
Invoke-DownloadScript $script:dgIntuneManagerObjects.SelectedItem
})
$buttonPanel.Children.Insert(0, $btn)
$btn = New-Object System.Windows.Controls.Button
$btn.Content = 'Edit'
$btn.Name = 'btnEdit'
$btn.Margin = "0,0,5,0"
$btn.Width = "100"
$btn.Add_Click({
Invoke-EditScript $script:dgIntuneManagerObjects.SelectedItem
})
$buttonPanel.Children.Insert(1, $btn)
}
##############################################
# Functions
##############################################
function Invoke-InitializeScriptUIExtensions {
$typeClasses = @()
Get-SubClasses "ScriptTypeBase" | ForEach-Object {
try {
$tmpClass = Get-SingletonObject $_.Name
if ($null -ne $tmpClass.PolicyGroup) {
$typeClasses += $tmpClass
}
}
catch {}
}
foreach ($typeClass in $typeClasses) {
Add-ObjectMethod $typeClass "AddUIExportExtensions" $SBAddScriptUIExportExtension
Add-ObjectMethod $typeClass "AddUIDetailsExtension" $SBAddScriptUIDetailsExtension
}
}
function Add-UIScriptExportExtensions {
param(
$Form,
$Description = "Export the script associated with selected profiles"
)
$grdExportProperties = $null
try {
$grdExportProperties = $Form.FindName("grdExportProperties")
}
catch {}
if (-not $grdExportProperties) { return }
if ($grdExportProperties.FindName("chkExportScript")) {
return # Already added
}
$xaml = @"
<StackPanel $($script:wpfNS) Orientation="Horizontal" Margin="0,0,5,0">
<Label Content="Export script" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="$Description" />
</StackPanel>
"@
try {
$label = [Windows.Markup.XamlReader]::Parse($xaml)
$chkExportScript = [System.Windows.Controls.CheckBox]::new()
$chkExportScript.IsChecked = $true
$chkExportScript.VerticalAlignment = "Center"
$chkExportScript.Name = "chkExportScript"
Set-CacheObject "ExportScripts" $chkExportScript.IsChecked
$rd = [System.Windows.Controls.RowDefinition]::new()
$rd.Height = [double]::NaN
$grdExportProperties.RowDefinitions.Add($rd)
$label.SetValue([System.Windows.Controls.Grid]::RowProperty, $grdExportProperties.RowDefinitions.Count - 1)
$chkExportScript.SetValue([System.Windows.Controls.Grid]::RowProperty, $grdExportProperties.RowDefinitions.Count - 1)
$chkExportScript.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 1)
$grdExportProperties.Children.Add($label)
$grdExportProperties.Children.Add($chkExportScript)
$Form.RegisterName($chkExportScript.Name, $chkExportScript)
$changed = [scriptblock] {
param($S, $E)
Set-CacheObject "ExportScripts" $this.IsChecked
}
$chkExportScript.Add_Checked($changed)
$chkExportScript.Add_Unchecked($changed)
}
catch {}
}
function Invoke-DownloadScript {
param($ScriptPolicy)
if (-not $ScriptPolicy) { return }
if ($ScriptPolicy.IsFullObject -eq $false) {
[void]$ScriptPolicy.Get()
}
if (-not $ScriptPolicy.JsonObject.scriptContent) { return }
Write-Status ""
$dlgSave = [System.Windows.Forms.SaveFileDialog]::new()
$dlgSave.InitialDirectory = Get-SettingValue "RootFolder" $env:Temp
$dlgSave.FileName = $ScriptPolicy.JsonObject.FileName
if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename)
{
Save-IntuneScriptContent $ScriptPolicy $dlgSave.FileName | Out-Null
}
}
function Invoke-EditScript {
param($ScriptPolicy)
if (-not $ScriptPolicy) { return }
if ($ScriptPolicy.IsFullObject -eq $false) {
[void]$ScriptPolicy.Get()
}
if (-not $ScriptPolicy.JsonObject.scriptContent) { return }
$script:currentScriptObject = $ScriptPolicy
$script:editForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\EditScriptDialog.xaml"))
if (-not $script:editForm) { return }
$script:UIProvider.SetXamlProperty($script:editForm, "txtEditScriptTitle", "Text", "Edit: $($ScriptPolicy.JsonObject.displayName)")
$scriptText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($ScriptPolicy.JsonObject.scriptContent))
$script:UIProvider.SetXamlProperty($script:editForm, "txtScriptText", "Text", $scriptText)
$script:currentModal = $null
if ($script:grdModal.Children.Count -gt 0) {
$script:currentModal = $script:grdModal.Children[0]
}
$script:UIProvider.AddXamlEvent($script:editForm, "btnSaveScriptEdit", "add_click", {
$scriptText = $script:UIProvider.GetXamlProperty($script:editForm, "txtScriptText", "Text")
$pre = [System.Text.Encoding]::UTF8.GetPreamble()
$utfBOM = [System.Text.Encoding]::UTF8.GetString($pre)
if ($scriptText.startsWith($utfBOM)) {
# Remove UTF8 BOM bytes
$scriptText = $scriptText.Remove(0, $utfBOM.Length)
}
$bytes = [System.Text.Encoding]::UTF8.GetBytes($scriptText)
$encodedText = [Convert]::ToBase64String($bytes)
if ($script:currentScriptObject.JsonObject.scriptContent -ne $encodedText) {
# Save script
if (($script:UIProvider.ShowMessageBox("Are you sure you want to update the script?`n`nObject:`n$($script:currentScriptObject.Name)", "Update script?", "YesNo", "Warning")) -eq "Yes") {
Write-Status "Update $($script:currentScriptObject.Name)"
$clonedObject = $script:currentScriptObject.Clone()
$clonedObject.JsonObject.scriptContent = $encodedText
Remove-GraphPropertiesForImport $clonedObject $clonedObject.JsonObject
foreach ($prop in $script:currentScriptObject.PolicyType.PropertiesToRemoveForUpdate) {
Remove-Property $clonedObject.JsonObject $prop
}
Remove-Property $clonedObject.JsonObject "Assignments"
Remove-Property $clonedObject.JsonObject "isAssigned"
if ($script:currentScriptObject.Set($null, $clonedObject.JsonString, "PATCH", $null)) {
Write-Log "Script saved successfully"
}
else {
Write-Log "Failed to save script" 3
}
Write-Status ""
}
}
$script:grdModal.Children.Clear()
if ($script:currentModal) {
$script:grdModal.Children.Add($script:currentModal)
}
[System.Windows.Forms.Application]::DoEvents()
})
$script:UIProvider.AddXamlEvent($script:editForm, "btnCancelScriptEdit", "add_click", {
$script:grdModal.Children.Clear()
if ($script:currentModal) {
$script:grdModal.Children.Add($script:currentModal)
}
[System.Windows.Forms.Application]::DoEvents()
})
$script:grdModal.Children.Clear()
$script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty, 1)
$script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 1)
$script:grdModal.Children.Add($script:editForm) | Out-Null
[System.Windows.Forms.Application]::DoEvents()
}
##############################################
# Initialize
##############################################
Invoke-InitializeScriptUIExtensions
+168
View File
@@ -0,0 +1,168 @@
#ImportOrder 5
class LogViewObject : ViewObjectBase
{
LogViewObject() : base()
{
$this.Init()
}
Hidden Init()
{
$this._ID = "CoreLog"
$this._Title = "Log"
$this._Description = "View log items"
$this._HideMenu = $true
}
[Object]GetViewPanel()
{
# ToDo: Remove recreate panel each time
#if($null -eq $this._ViewPanel) {
$this._ViewPanel = Get-LogViewPanel
#}
return $this._ViewPanel
}
[Object[]]GetViewItems()
{
return $null
}
Authenticate()
{
}
[Object[]]OnItemChanged($SelectedItem)
{
return $null
}
OnDeactivating($NewActiveView)
{
}
OnActivating($PreviousActiveView)
{
}
OnActivated()
{
}
}
class CachedObjectsViewObject : ViewObjectBase
{
CachedObjectsViewObject() : base()
{
$this.Init()
}
Hidden Init()
{
$this._ID = "CoreCachedObjects"
$this._Title = "Cached Objects"
$this._Description = "View cached items"
$this._HideMenu = $true
}
[Object]GetViewPanel()
{
# ToDo: Remove recreate panel each time
#if($null -eq $this._ViewPanel) {
$this._ViewPanel = Get-CachedObjectsViewPanel
#}
return $this._ViewPanel
}
[Object[]]GetViewItems()
{
return $null
}
Authenticate()
{
}
[Object[]]OnItemChanged($SelectedItem)
{
return $null
}
OnDeactivating($NewActiveView)
{
}
OnActivating($PreviousActiveView)
{
}
OnActivated()
{
if($this._ViewPanel) {
Update-CachedObjectsView -ViewPanel $this._ViewPanel
}
}
}
class GraphCallsViewObject : ViewObjectBase
{
GraphCallsViewObject() : base()
{
$this.Init()
}
Hidden Init()
{
$this._ID = "GraphCalls"
$this._Title = "Graph Calls"
$this._Description = "View all calls to Microsoft Graph"
$this._HideMenu = $true
}
[Object]GetViewPanel()
{
# ToDo: Remove recreate panel each time
#if($null -eq $this._ViewPanel) {
$this._ViewPanel = Get-GraphCallsViewPanel
#}
return $this._ViewPanel
}
[Object[]]GetViewItems()
{
return $null
}
Authenticate()
{
}
[Object[]]OnItemChanged($SelectedItem)
{
return $null
}
OnDeactivating($NewActiveView)
{
}
OnActivating($PreviousActiveView)
{
}
OnActivated()
{
}
}
@@ -0,0 +1,51 @@
#ImportOrder 160
class IntuneToolsViewObject : ViewObjectBase
{
IntuneToolsViewObject() : Base()
{
$this.Init()
}
Hidden Init()
{
$this._ID = "IntuneTools"
$this._Title = "Intune Tools"
$this._Description = "Tools that operate on Intune objects, such as listing assignments across all object types."
$this._AddToMenu = $true
# Tools list is short and curated — fine to also surface in the top
# Views menu as a submenu (one entry per tool, grouped by Category).
$this._ExpandInViewsMenu = $true
}
[Object]GetViewPanel()
{
if($null -eq $this._ViewPanel)
{
$this._ViewPanel = Get-IntuneToolsViewPanel
}
return $this._ViewPanel
}
[PSCustomObject[]]GetViewItems()
{
return @(Get-IntuneToolsViewItems)
}
Authenticate()
{
# Auto-login policy: silent-only. If the silent path fails the user signs in
# explicitly via the Login button — we never pop an unbidden OAuth window
# from a view-activation event.
#
# Also gated to MSAL provider so MgGraph mode isn't hijacked by a successful
# MSAL silent auth here.
$active = $null
try { $active = Get-AuthProvider } catch { }
if($active) { $active.RefreshAmbientSession() }
}
[Object[]]OnItemChanged($SelectedItem)
{
return (Invoke-IntuneToolsActivateItem $SelectedItem)
}
}
+61
View File
@@ -0,0 +1,61 @@
#ImportOrder 150
class IntuneViewObject : ViewObjectBase
{
Hidden [IntunePolicyBase[]]$_Items = @()
IntuneViewObject() : Base()
{
$this.Init()
}
Hidden Init()
{
$this._ID = "IntuneManagement"
$this._Title = "Intune Manager"
$this._Description = "Manages Intune environments. This view can be used for copying objects in an Intune environment. It can also be used for backing up an entire Intune environment and cloning the Intune environment into another tenant."
$this._AddToMenu = $false
}
[Object]GetViewPanel()
{
if($null -eq $this._ViewPanel) {
$this._ViewPanel = Get-IntuneManagementViewPanel
}
return $this._ViewPanel
}
[PSCustomObject[]]GetViewItems()
{
return @(Get-IntuneViewItems)
}
Authenticate()
{
# Auto-login policy: at startup / view activation we only attempt SILENT auth
# against the last-logged-on account. If it fails we surface "signed out" and
# wait for the user to click the Login button. We do NOT pop an interactive
# OAuth prompt unbidden — that would steal focus from whatever the user is
# doing and contradict the explicit-login design.
#
# Also gated to MSAL provider so MgGraph mode isn't hijacked (a successful
# silent MSAL auth here would flip the active provider).
$active = $null
try { $active = Get-AuthProvider } catch { }
if($active) { $active.RefreshAmbientSession() }
}
[Object[]]OnItemChanged($SelectedItem)
{
return (Invoke-IntuneActivateObject $SelectedItem)
}
OnDeactivating($NewActiveView)
{
Invoke-IntuneDeactivatingView $NewActiveView
}
OnActivating($PreviousActiveView)
{
Invoke-IntuneActivatingView $PreviousActiveView
}
}
+218
View File
@@ -0,0 +1,218 @@
#ImportOrder 60
#
# WPF concrete subclass of UIProvider. Each method captures the
# corresponding free function from CoreUIWPF.ps1 (and friends) at construction
# time via ${function:X}, then invokes the captured scriptblock with `&`.
# This is defensive: PS native class methods *may* see module-scoped free
# functions, but the documented workaround for ScriptMethod scriptblocks
# (see [[scriptmethod-loses-module-scope]]) is the capture pattern. Until a
# smoke test confirms direct calls work, we capture explicitly.
#
# Folder picking delegates to the existing WPF Get-Folder helper. Open/save
# file dialogs are intentionally not part of the base provider until both
# backends have an implemented contract.
class WPFUIProvider : UIProvider {
#region Modal / dialog
[void] ShowMessageBox([string]$Text) {
$f = ${function:Show-MessageBox}
& $f $Text | Out-Null
}
[void] ShowMessageBox([string]$Text, [string]$Caption) {
$f = ${function:Show-MessageBox}
& $f $Text $Caption | Out-Null
}
[object] ShowMessageBox([string]$Text, [string]$Caption, [string]$Button, [string]$Icon) {
$f = ${function:Show-MessageBox}
return (& $f $Text $Caption $Button $Icon)
}
[void] ShowModalForm([string]$FormTitle, [object]$FormObject) {
$f = ${function:Show-ModalForm}
& $f $FormTitle $FormObject
}
[void] ShowModalForm([string]$FormTitle, [object]$FormObject, [bool]$HideButtons) {
$f = ${function:Show-ModalForm}
if ($HideButtons) { & $f $FormTitle $FormObject -HideButtons }
else { & $f $FormTitle $FormObject }
}
[void] ShowModalObject() {
$f = ${function:Show-ModalObject}
& $f
}
[void] ShowModalObject([object]$Obj) {
$f = ${function:Show-ModalObject}
& $f $Obj
}
[void] CloseTopModalObject() {
$f = ${function:Close-TopModalObject}
& $f
}
[string] ShowInputDialog([string]$FormTitle, [string]$FormText, [string]$DefaultValue) {
$f = ${function:Show-InputDialog}
return (& $f $FormTitle $FormText $DefaultValue)
}
[void] ShowAboutDialog() {
$f = ${function:Show-AboutDialog}
& $f
}
[bool] RequestUIConfirmation([string]$Message, [string]$Caption) {
$f = ${function:Request-UIConfirmation}
return [bool](& $f $Message $Caption)
}
[void] ShowPopup([object]$Popup) {
$f = ${function:Show-Popup}
& $f $Popup
}
[void] HidePopup() {
$f = ${function:Hide-Popup}
& $f
}
#endregion
#region File / folder pickers
[string] ShowFolderPicker([string]$Description) {
$f = ${function:Get-Folder}
return (& $f -Title $Description)
}
[string] ShowFolderPicker([string]$Path, [string]$Description) {
$f = ${function:Get-Folder}
return (& $f -Path $Path -Title $Description)
}
#endregion
#region XAML helpers
[object] GetXamlObject([string]$FileName) {
$f = ${function:Get-XamlObject}
return (& $f $FileName)
}
[object] GetXamlObject([string]$FileName, [bool]$AddVariables) {
$f = ${function:Get-XamlObject}
if ($AddVariables) { return (& $f $FileName -AddVariables) }
else { return (& $f $FileName) }
}
[object] GetXamlObject([string]$FileName, [bool]$AddVariables, [bool]$AddStyles) {
$f = ${function:Get-XamlObject}
$h = @{}
if ($AddVariables) { $h.AddVariables = $true }
if ($AddStyles) { $h.AddStyles = $true }
return (& $f $FileName @h)
}
[void] AddXamlEvent([object]$XamlObj, [string]$ControlName, [string]$EventName, [scriptblock]$Handler) {
$f = ${function:Add-XamlEvent}
& $f $XamlObj $ControlName $EventName $Handler
}
[void] AddXamlVariables([object]$XamlObj, [object]$Scope) {
$f = ${function:Add-XamlVariables}
& $f $XamlObj $Scope
}
[void] SetXamlProperty([object]$XamlObj, [string]$ControlName, [string]$PropertyName, [object]$Value) {
$f = ${function:Set-XamlProperty}
& $f $XamlObj $ControlName $PropertyName $Value
}
[object] GetXamlProperty([object]$XamlObj, [string]$ControlName, [string]$PropertyName) {
$f = ${function:Get-XamlProperty}
return (& $f $XamlObj $ControlName $PropertyName)
}
[void] SetControlVisible([object]$XamlObj, [string]$ControlName, [bool]$Visible) {
$value = if ($Visible) { "Visible" } else { "Collapsed" }
$this.SetXamlProperty($XamlObj, $ControlName, "Visibility", $value)
}
#endregion
#region Main window / chrome
[void] ShowMainWindow([object]$View) {
$f = ${function:Show-MainWindow}
& $f $View
}
[void] SetMainTitle() {
$f = ${function:Set-MainTitle}
& $f
}
[object] GetMainWindow() {
$f = ${function:Get-MainWindow}
return (& $f)
}
[void] SetAppTheme([string]$ThemeName) {
$f = ${function:Set-AppTheme}
& $f $ThemeName
}
#endregion
#region Threading / status
[void] UpdateUIStatus([string]$Text) {
$f = ${function:Update-UIStatus}
& $f -Text $Text | Out-Null
}
[bool] UpdateUIStatus([System.Collections.IDictionary]$Params) {
$f = ${function:Update-UIStatus}
$h = @{}
foreach ($k in $Params.Keys) { $h[$k] = $Params[$k] }
return [bool](& $f @h)
}
[void] InvokeUIMessagePump() {
$f = ${function:Invoke-UIMessagePump}
& $f
}
[void] SetClipboardText([string]$Text) {
# Clipboard.SetText throws on null/empty; Clear() is the empty equivalent.
if([string]::IsNullOrEmpty($Text)) { [System.Windows.Clipboard]::Clear(); return }
[System.Windows.Clipboard]::SetText($Text)
}
#endregion
#region Auth chrome
[void] ShowAuthenticationInfo() {
$f = ${function:Show-AuthenticationInfo}
& $f
}
[void] SetEnvironmentInfo() {
$f = ${function:Set-EnvironmentInfo}
& $f
}
[void] SetEnvironmentInfo([string]$TenantName) {
$f = ${function:Set-EnvironmentInfo}
& $f $TenantName
}
#endregion
}
@@ -0,0 +1,18 @@
# Provider-agnostic authentication UI helpers.
#
# Phase 1 placeholder. The profile popup and account picker still live in
# MSGraphAuthenticationUI.ps1 and read MSAL state directly. Phase 2 will migrate
# them here once the MgGraph provider lands and we have a second consumer to
# validate the abstraction.
#
# When that happens, expect these helpers to live here:
# Show-AuthProviderProfilePopup - build the popup from $provider.GetUserInfo
# Add-AuthProviderCachedAccountRow - render a row for $provider.GetCachedAccounts
# Add-AuthProviderTenantRow - tenant switcher from $provider.GetAvailableTenants
# Update-AuthProviderUI - refresh the title-bar profile picture
#
# All of these will route through Get-AuthProvider so they work for any registered
# provider with no per-provider UI code.
# Currently empty by design. Do not delete — the file is part of the auth
# abstraction's stable file layout and Phase 2 will populate it.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
# "Permissions" button in the Profile popup: what the signed-in identity can
# actually do per policy type - the app's token scopes combined with the user's
# Intune role (Internal/EffectivePermissions.ps1). Thin caller of the public
# Get-GraphEffectivePermissions (R10); the popup wiring in
# MSGraphAuthenticationUIWPF.ps1 is a single AddXamlEvent line.
function Show-EffectivePermissionsDialog {
param()
$rows = @()
$raw = $null
try {
$rows = @(Get-GraphEffectivePermissions)
$raw = Get-GraphEffectivePermissions -Raw
}
catch { Write-LogError "Get-GraphEffectivePermissions failed" $_.Exception }
if($rows.Count -eq 0) {
$script:UIProvider.ShowMessageBox("No access token available. Sign in first.", "Permissions", "OK", "Information") | Out-Null
return
}
$header = Get-EffectivePermissionsHeaderText $raw
$grid = [System.Windows.Controls.Grid]::new()
$grid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{ Height = "Auto" })
$grid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{ Height = "*" })
$txt = [System.Windows.Controls.TextBlock]::new()
$txt.Text = $header
$txt.TextWrapping = "Wrap"
$txt.Margin = "5,5,5,10"
[System.Windows.Controls.Grid]::SetRow($txt, 0)
$grid.Children.Add($txt) | Out-Null
$dg = [System.Windows.Controls.DataGrid]::new()
Set-GridPixelScrolling $dg
$dg.IsReadOnly = $true
$dg.ItemsSource = @($rows | Sort-Object EffectiveLevel, Title |
Select-Object @{ n = "Type"; e = { $_.Title } },
@{ n = "Category"; e = { $_.ResourceCategory } },
@{ n = "Required"; e = { $_.Required } },
@{ n = "Token"; e = { $_.TokenAccess } },
@{ n = "Intune role"; e = { $_.RoleAccess } },
@{ n = "Effective"; e = { $_.EffectiveAccess } },
@{ n = "Result"; e = { $_.Result } },
Reason)
[System.Windows.Controls.Grid]::SetRow($dg, 1)
$grid.Children.Add($dg) | Out-Null
$script:UIProvider.ShowModalForm("Permissions", $grid)
}
# One-paragraph explanation of where the Intune-role half came from. Shared
# wording with the Avalonia dialog lives here in spirit only - each backend
# keeps its own copy under UI/<backend>/ (R12).
function Get-EffectivePermissionsHeaderText {
param($Context)
$refresh = "Changed roles (PIM, a new Intune role)? Click Refresh in the Profile popup - the app keeps using the token it signed in with until then."
if(-not $Context) {
return ("Token scopes only. The Intune role check does not apply to this token (app-only sign-in) or " +
"could not be read; see the log. Scope tags are not evaluated. $refresh")
}
$asOf = if($Context.AsOf) { " Token issued $($Context.AsOf.ToString('yyyy-MM-dd HH:mm'))." } else { "" }
if($Context.AllAllowed) {
return ("Token scopes combined with the Intune Administrator / Global Administrator directory role " +
"(full Intune access, no per-action lookup).$asOf $refresh")
}
# "N of M" only when the action catalogue was readable; M is how many actions
# Intune defines, so N of N would mean full access, not an empty answer.
$counts = "$($Context.Allowed.Count) resource actions allowed"
if($Context.Catalog) { $counts = "$($Context.Allowed.Count) of $($Context.Catalog.Count) resource actions allowed" }
return ("Token scopes combined with the signed-in user's Intune role permissions " +
"($counts).$asOf " +
"Scope tags are not evaluated: a user limited to some tags can still be refused on individual objects. $refresh")
}
@@ -0,0 +1,783 @@
# Bulk Assignments form + assignment-row helpers.
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Show-GraphBulkAssignmentsForm
{
$script:bulkAssignForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkAssignmentsForm.xaml"))
if(-not $script:bulkAssignForm) { return }
$script:dgBulkAssignObjects = $script:bulkAssignForm.FindName("dgBulkAssignObjects")
$script:dgBulkAssignList = $script:bulkAssignForm.FindName("dgBulkAssignList")
$script:txtBulkAssignStatus = $script:bulkAssignForm.FindName("txtBulkAssignStatus")
$assignmentSettings = [IntuneManagerAssignmentSettings]::new()
$script:bulkAssignForm.DataContext = $assignmentSettings
# ── Eligible types: use the same support gate as Set-GraphBulkAssignments
# so the UI does not offer groups/APIs that the command will skip.
$script:bulkAssignEligibleTypes = @($script:IntuneTypes | Where-Object {
Test-BulkAssignmentSupported $_
})
$eligibleTypeIds = @($script:bulkAssignEligibleTypes | ForEach-Object { $_.Id })
$script:bulkAssignEligibleGroups = @($script:IntuneGroups | Where-Object {
$_.Title -and ($_.PolicyTypes | Where-Object { $_.Id -in $eligibleTypeIds })
})
# ── Object list (mirrors Bulk Scope Tag DataGrid setup) ──────────────────
$column = Get-GridCheckboxColumn "Selected"
$script:dgBulkAssignObjects.Columns.Add($column)
$script:bulkAssignSelectAllHeader = $column.Header
$script:bulkAssignSelectAllHeader.IsChecked = $true
$script:bulkAssignSelectAllHeader.add_Click({
foreach($item in $script:dgBulkAssignObjects.ItemsSource) {
$item.Selected = $this.IsChecked
}
$script:dgBulkAssignObjects.Items.Refresh()
})
$col = [System.Windows.Controls.DataGridTextColumn]::new()
$col.Header = "Object type"
$col.IsReadOnly = $true
$col.Binding = [System.Windows.Data.Binding]::new("Title")
$script:dgBulkAssignObjects.Columns.Add($col)
$script:bulkAssignMode = "Group"
Update-BulkAssignObjectList
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "rbBulkAssignViewGroup", "add_Checked", {
$script:bulkAssignMode = "Group"; Update-BulkAssignObjectList
if($script:bulkAssignSelectAllHeader) { $script:bulkAssignSelectAllHeader.IsChecked = $true }
})
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "rbBulkAssignViewType", "add_Checked", {
$script:bulkAssignMode = "Type"; Update-BulkAssignObjectList
if($script:bulkAssignSelectAllHeader) { $script:bulkAssignSelectAllHeader.IsChecked = $true }
})
# ── Action radio → settings.Action sync ───────────────────────────────────
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "rbBulkAssignAdd", "add_Checked", { $script:bulkAssignForm.DataContext.Action = "Add" })
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "rbBulkAssignReplace", "add_Checked", { $script:bulkAssignForm.DataContext.Action = "Replace" })
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "rbBulkAssignRemove", "add_Checked", { $script:bulkAssignForm.DataContext.Action = "Remove" })
# ── Assignments list — backing ObservableCollection so adds/removes show
# immediately. Each row carries display strings + the raw fields the
# public command consumes.
$script:colBulkAssignList = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
$script:dgBulkAssignList.ItemsSource = $script:colBulkAssignList
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "btnBulkAssignAddGroup", "add_click", {
Show-BulkAssignmentGroupPicker
})
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "btnBulkAssignAddAllDev", "add_click", {
Add-BulkAssignmentRow -TargetType "allDevicesAssignmentTarget" -GroupName "All Devices"
})
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "btnBulkAssignAddAllUsers", "add_click", {
Add-BulkAssignmentRow -TargetType "allLicensedUsersAssignmentTarget" -GroupName "All Users"
})
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "btnBulkAssignSettings", "add_click", {
$sel = $script:dgBulkAssignList.SelectedItem
if(-not $sel) {
$script:UIProvider.ShowMessageBox("Select an assignment row first.", "Bulk Assignments", "OK", "Information") | Out-Null
return
}
Show-BulkAssignmentSettingsDialog -Row $sel
})
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "btnBulkAssignRemoveSel", "add_click", {
$sel = $script:dgBulkAssignList.SelectedItem
if($sel) { [void]$script:colBulkAssignList.Remove($sel) }
})
# Double-click opens the settings dialog (less destructive than remove).
$script:dgBulkAssignList.Add_MouseDoubleClick({
$sel = $script:dgBulkAssignList.SelectedItem
if($sel) { Show-BulkAssignmentSettingsDialog -Row $sel }
})
# ── Apply ─────────────────────────────────────────────────────────────────
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "btnBulkAssignApply", "add_click", {
$btnApply = $script:bulkAssignForm.FindName("btnBulkAssignApply")
$btnClose = $script:bulkAssignForm.FindName("btnBulkAssignClose")
$selectionIds = Get-BulkAssignSelectedObjectIds
$unit = if($script:bulkAssignMode -eq "Type") { "policy type" } else { "object group" }
if($selectionIds.Count -eq 0) {
$script:UIProvider.ShowMessageBox("Select at least one $unit to update.", "Bulk Assignments", "OK", "Warning") | Out-Null
return
}
$settings = $script:bulkAssignForm.DataContext
$settings.Assignments = @($script:colBulkAssignList)
if($settings.Assignments.Count -eq 0) {
$script:UIProvider.ShowMessageBox("Add at least one assignment target before applying.", "Bulk Assignments", "OK", "Warning") | Out-Null
return
}
# Mass-wipe guard: Replace with one row that isn't the actual desired
# final state still wipes everything else. Warn before continuing.
if($settings.Action -eq "Replace") {
$proceed = $script:UIProvider.ShowMessageBox(
"Replace will OVERWRITE every existing assignment on every matched policy with only the rows above ($($settings.Assignments.Count) target(s)).`n`nContinue?",
"Confirm replace", "YesNo", "Warning")
if($proceed -ne "Yes") { return }
}
$assignmentSummary = ($settings.Assignments | ForEach-Object {
$parts = @($_.TargetTypeDisplay, $_.GroupName)
if($_.FilterId) { $parts += "filter: $($_.FilterName) ($($_.FilterType))" }
($parts -join ' / ')
}) -join "`n "
$filterText = if($settings.Filter) { $settings.Filter } else { "(none)" }
$confirm = $script:UIProvider.ShowMessageBox(@"
About to update assignments on every policy that matches:
Action : $($settings.Action)
Assignments :
$assignmentSummary
Name filter : $filterText
$unit count : $($selectionIds.Count)
Continue?
"@, "Confirm Bulk Assignment update", "YesNo", "Warning")
if($confirm -ne "Yes") { return }
if($btnApply) { $btnApply.IsEnabled = $false }
if($btnClose) { $btnClose.IsEnabled = $false }
# No pre-call Write-Status: Set-GraphBulkAssignments now sets its own two-line
# status ("Bulk assignments — <Action>" + sub-step) as soon as it starts.
$startParams = @{ AssignmentSettings = $settings }
if($script:bulkAssignMode -eq "Type") { $startParams.PolicyType = $selectionIds }
else { $startParams.PolicyGroup = $selectionIds }
try {
$summary = Set-GraphBulkAssignments @startParams
Write-Status $null
$unsupportedNote = if($summary.UnsupportedTypes -and $summary.UnsupportedTypes.Count -gt 0) {
"`nUnsupported types skipped: $($summary.UnsupportedTypes -join ', ')"
} else { "" }
$msg = ("Scanned: {0}`nMatched filter: {1}`nUpdated: {2}`nNo change: {3}`nFailed: {4}`nDuration: {5:hh\:mm\:ss}{6}" -f `
$summary.PoliciesScanned, $summary.PoliciesMatched, $summary.PoliciesUpdated, $summary.PoliciesSkipped, $summary.PoliciesFailed, $summary.Duration, $unsupportedNote)
$script:txtBulkAssignStatus.Text = ("Last run: updated {0}, no change {1}, failed {2}" -f $summary.PoliciesUpdated, $summary.PoliciesSkipped, $summary.PoliciesFailed)
$unknownNote = Get-IntuneUnknownSelectorSummary $summary.UnknownSelectors
if($unknownNote) { $msg += "`n`n$unknownNote" }
$script:UIProvider.ShowMessageBox($msg, "Bulk Assignments", "OK", "Information") | Out-Null
try { $script:dgIntuneManagerObjects.Items.Refresh() }
catch { Write-LogDebug "Main grid refresh failed after bulk assignment run: $($_.Exception.Message)" }
}
catch {
Write-LogError "Bulk Assignments run failed" $_.Exception
$script:UIProvider.ShowMessageBox("Bulk Assignments run failed: $($_.Exception.Message)", "Bulk Assignments", "OK", "Error") | Out-Null
}
finally {
if($btnApply) { $btnApply.IsEnabled = $true }
if($btnClose) { $btnClose.IsEnabled = $true }
}
})
$script:UIProvider.AddXamlEvent($script:bulkAssignForm, "btnBulkAssignClose", "add_click", {
$script:bulkAssignForm = $null
$script:bulkAssignSelectAllHeader = $null
Show-ModalObject
})
$script:UIProvider.ShowModalForm("Bulk Assignments", $script:bulkAssignForm, $true)
}
# Rebuild the bulk-assign object DataGrid for the current view mode.
function Update-BulkAssignObjectList
{
if(-not $script:dgBulkAssignObjects) { return }
$script:bulkAssignObjects = @()
if($script:bulkAssignMode -eq "Type") {
$sorted = $script:bulkAssignEligibleTypes | Sort-Object Title
foreach($pt in $sorted) {
$script:bulkAssignObjects += New-Object PSObject -Property @{
Title = $pt.Title
Selected = $true
ObjectGroup = $null
ObjectType = $pt
}
}
}
else {
$sorted = $script:bulkAssignEligibleGroups | Sort-Object Title
foreach($grp in $sorted) {
$script:bulkAssignObjects += New-Object PSObject -Property @{
Title = $grp.Title
Selected = $true
ObjectGroup = $grp
ObjectType = $null
}
}
}
$script:dgBulkAssignObjects.ItemsSource = $script:bulkAssignObjects
}
function Get-BulkAssignSelectedObjectIds
{
if($script:bulkAssignMode -eq "Type") {
return @($script:bulkAssignObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectType -and $_.ObjectType.Id } |
ForEach-Object { $_.ObjectType.Id })
}
return @($script:bulkAssignObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectGroup -and $_.ObjectGroup.Id } |
ForEach-Object { $_.ObjectGroup.Id })
}
# Append a row to the assignments DataGrid. Used by All Devices / All Users
# buttons and by the group picker dialog after the user picks OK.
function Add-BulkAssignmentRow
{
param(
[Parameter(Mandatory)][string]$TargetType,
[string]$GroupId,
[string]$GroupName,
[string]$FilterId,
[string]$FilterName,
[string]$FilterType = "include",
# installIntent for mobileAppAssignment. Ignored by the public command
# for non-app types, so safe to set on every row.
[string]$Intent = "required"
)
# Display strings — keep the data layer plain (TargetType is the raw
# @odata.type fragment the public command uses; *Display columns are
# what the DataGrid binds to).
$typeDisplay = switch ($TargetType) {
"groupAssignmentTarget" { "Include group" }
"exclusionGroupAssignmentTarget" { "Exclude group" }
"allDevicesAssignmentTarget" { "All devices" }
"allLicensedUsersAssignmentTarget" { "All users" }
default { $TargetType }
}
$filterDisplay = if($FilterId) { "$FilterName ($FilterType)" } else { "" }
$row = [PSCustomObject]@{
TargetType = $TargetType
TargetTypeDisplay = $typeDisplay
GroupId = $GroupId
GroupName = if($GroupName) { $GroupName } else { $GroupId }
FilterId = $FilterId
FilterName = $FilterName
FilterType = $FilterType
FilterDisplay = $filterDisplay
Intent = $Intent
# Per-platform app assignment settings, keyed by Graph settings type
# name (e.g. "win32LobAppAssignmentSettings"). Absent key = no override
# for that type → Graph defaults are used on POST. Populated via the
# App settings... dialog (Show-BulkAssignmentSettingsDialog).
Settings = @{}
SettingsDisplay = "(none)"
}
# Dedupe — don't add the same target+intent twice. Two rows with the
# same target but different intents are intentionally allowed (Required
# vs Available is meaningful for apps).
$existing = @($script:colBulkAssignList | Where-Object {
$_.TargetType -eq $row.TargetType -and
[string]$_.GroupId -eq [string]$row.GroupId -and
[string]$_.FilterId -eq [string]$row.FilterId -and
([string]::IsNullOrEmpty($row.FilterId) -or $_.FilterType -eq $row.FilterType) -and
[string]$_.Intent -eq [string]$row.Intent
})
if($existing.Count -gt 0) {
$script:UIProvider.ShowMessageBox("That target is already in the list (with the same intent).", "Bulk Assignments", "OK", "Information") | Out-Null
return
}
[void]$script:colBulkAssignList.Add($row)
}
# ─── Bulk Assignments — per-platform settings ────────────────────────────────
#
# Schema for each settings type lives in $script:_bulkAssignSettingsSchema —
# one entry per TabItem in BulkAssignmentsSettings.xaml. Drives both the load
# (Row.Settings → UI controls) and save (UI controls → Row.Settings) passes
# from one piece of code per direction.
#
# Field types:
# bool — CheckBox.IsChecked
# string — TextBox.Text (empty skipped)
# int — TextBox.Text → int (empty skipped, non-numeric skipped)
# enum — ComboBox.SelectedItem (string)
# datetime — TextBox.Text passed through as-is (Graph accepts ISO8601)
# stringList — TextBox.Text split on newline / semicolon / comma (empties skipped)
#
# Nested complex types (win32LobAppRestartSettings, etc.) are encoded under a
# Nested array on the tab descriptor; the saved hashtable gets a nested
# hashtable + @odata.type stamp.
$script:_bulkAssignSettingsSchema = @(
@{ Apply = "chkApplyIosLob"; Type = "iosLobAppAssignmentSettings";
Fields = @(
@{ Control="chkIosLob_IsRemovable"; Key="isRemovable"; Kind="bool" }
@{ Control="chkIosLob_PreventBackup"; Key="preventManagedAppBackup"; Kind="bool" }
@{ Control="chkIosLob_Uninstall"; Key="uninstallOnDeviceRemoval"; Kind="bool" }
@{ Control="txtIosLob_VpnId"; Key="vpnConfigurationId"; Kind="string" }
)}
@{ Apply = "chkApplyIosStore"; Type = "iosStoreAppAssignmentSettings";
Fields = @(
@{ Control="chkIosStore_IsRemovable"; Key="isRemovable"; Kind="bool" }
@{ Control="chkIosStore_PreventBackup"; Key="preventManagedAppBackup"; Kind="bool" }
@{ Control="chkIosStore_Uninstall"; Key="uninstallOnDeviceRemoval"; Kind="bool" }
@{ Control="txtIosStore_VpnId"; Key="vpnConfigurationId"; Kind="string" }
)}
@{ Apply = "chkApplyIosVpp"; Type = "iosVppAppAssignmentSettings";
Fields = @(
@{ Control="chkIosVpp_DeviceLicensing"; Key="useDeviceLicensing"; Kind="bool" }
@{ Control="chkIosVpp_IsRemovable"; Key="isRemovable"; Kind="bool" }
@{ Control="chkIosVpp_PreventBackup"; Key="preventManagedAppBackup"; Kind="bool" }
@{ Control="chkIosVpp_PreventAutoUpdate"; Key="preventAutoAppUpdate"; Kind="bool" }
@{ Control="chkIosVpp_Uninstall"; Key="uninstallOnDeviceRemoval"; Kind="bool" }
@{ Control="txtIosVpp_VpnId"; Key="vpnConfigurationId"; Kind="string" }
)}
@{ Apply = "chkApplyIosDdm"; Type = "iosDdmLobAppAssignmentSettings";
Fields = @(
@{ Control="txtIosDdm_Domains"; Key="associatedDomains"; Kind="stringList" }
@{ Control="chkIosDdm_DirectDownload"; Key="associatedDomainsDirectDownloadAllowed"; Kind="bool" }
@{ Control="chkIosDdm_PreventBackup"; Key="preventManagedAppBackup"; Kind="bool" }
@{ Control="chkIosDdm_TapToPay"; Key="tapToPayScreenLockEnabled"; Kind="bool" }
@{ Control="txtIosDdm_VpnId"; Key="vpnConfigurationId"; Kind="string" }
)}
@{ Apply = "chkApplyAndroidStore"; Type = "androidManagedStoreAppAssignmentSettings";
Fields = @(
@{ Control="cbAndroidStore_AutoUpdate"; Key="autoUpdateMode"; Kind="enum" }
@{ Control="txtAndroidStore_Tracks"; Key="androidManagedStoreAppTrackIds"; Kind="stringList" }
)}
@{ Apply = "chkApplyMacLob"; Type = "macOsLobAppAssignmentSettings";
Fields = @(
@{ Control="chkMacLob_Uninstall"; Key="uninstallOnDeviceRemoval"; Kind="bool" }
)}
@{ Apply = "chkApplyMacVpp"; Type = "macOsVppAppAssignmentSettings";
Fields = @(
@{ Control="chkMacVpp_DeviceLicensing"; Key="useDeviceLicensing"; Kind="bool" }
@{ Control="chkMacVpp_PreventBackup"; Key="preventManagedAppBackup"; Kind="bool" }
@{ Control="chkMacVpp_PreventAutoUpdate"; Key="preventAutoAppUpdate"; Kind="bool" }
@{ Control="chkMacVpp_Uninstall"; Key="uninstallOnDeviceRemoval"; Kind="bool" }
)}
@{ Apply = "chkApplyMsStore"; Type = "microsoftStoreForBusinessAppAssignmentSettings";
Fields = @(
@{ Control="chkMsStore_DeviceContext"; Key="useDeviceContext"; Kind="bool" }
)}
@{ Apply = "chkApplyWinAppx"; Type = "windowsAppXAppAssignmentSettings";
Fields = @(
@{ Control="chkWinAppx_DeviceContext"; Key="useDeviceContext"; Kind="bool" }
)}
@{ Apply = "chkApplyWinUap"; Type = "windowsUniversalAppXAppAssignmentSettings";
Fields = @(
@{ Control="chkWinUap_DeviceContext"; Key="useDeviceContext"; Kind="bool" }
)}
@{ Apply = "chkApplyWin32"; Type = "win32LobAppAssignmentSettings";
Fields = @(
@{ Control="cbWin32_DeliveryOpt"; Key="deliveryOptimizationPriority"; Kind="enum" }
@{ Control="cbWin32_Notifications"; Key="notifications"; Kind="enum" }
)
Nested = @(
@{ Key="autoUpdateSettings"; Type="win32LobAppAutoUpdateSettings";
Fields=@(
@{ Control="cbWin32_Superseded"; Key="autoUpdateSupersededAppsState"; Kind="enum" }
)}
@{ Key="installTimeSettings"; Type="mobileAppInstallTimeSettings";
Fields=@(
@{ Control="chkWin32_UseLocalTime"; Key="useLocalTime"; Kind="bool" }
@{ Control="txtWin32_StartTime"; Key="startDateTime"; Kind="datetime" }
@{ Control="txtWin32_DeadlineTime"; Key="deadlineDateTime"; Kind="datetime" }
)}
@{ Key="restartSettings"; Type="win32LobAppRestartSettings";
Fields=@(
@{ Control="txtWin32_GracePeriod"; Key="gracePeriodInMinutes"; Kind="int" }
@{ Control="txtWin32_Countdown"; Key="countdownDisplayBeforeRestartInMinutes"; Kind="int" }
)}
)}
@{ Apply = "chkApplyWinGet"; Type = "winGetAppAssignmentSettings";
Fields = @(
@{ Control="cbWinGet_Notifications"; Key="notifications"; Kind="enum" }
)
Nested = @(
@{ Key="installTimeSettings"; Type="winGetAppInstallTimeSettings";
Fields=@(
@{ Control="chkWinGet_UseLocalTime"; Key="useLocalTime"; Kind="bool" }
@{ Control="txtWinGet_DeadlineTime"; Key="deadlineDateTime"; Kind="datetime" }
)}
@{ Key="restartSettings"; Type="winGetAppRestartSettings";
Fields=@(
@{ Control="txtWinGet_GracePeriod"; Key="gracePeriodInMinutes"; Kind="int" }
@{ Control="txtWinGet_Countdown"; Key="countdownDisplayBeforeRestartInMinutes"; Kind="int" }
)}
)}
@{ Apply = "chkApplyWinAutoUpdate"; Type = "windowsAutoUpdateCatalogAppAssignmentSettings";
Fields = @(
@{ Control="cbWinAutoUpdate_DeliveryOpt"; Key="deliveryOptimizationPriority"; Kind="enum" }
@{ Control="cbWinAutoUpdate_Notifications"; Key="notificationType"; Kind="enum" }
)
Nested = @(
@{ Key="installTimeSettings"; Type="windowsAutoUpdateCatalogAppInstallTimeSettings";
Fields=@(
@{ Control="chkWinAutoUpdate_UseLocalTime"; Key="useLocalTime"; Kind="bool" }
@{ Control="txtWinAutoUpdate_StartTime"; Key="startDateTime"; Kind="datetime" }
@{ Control="txtWinAutoUpdate_DeadlineTime"; Key="deadlineDateTime"; Kind="datetime" }
)}
@{ Key="restartSettings"; Type="windowsAutoUpdateCatalogAppRestartSettings";
Fields=@(
@{ Control="txtWinAutoUpdate_GracePeriod"; Key="gracePeriodInMinutes"; Kind="int" }
)}
)}
)
# Pull a field value out of a control and convert to the right Graph type.
# Returns $null when the field has no usable value (empty string, no
# selection, parse failure) — caller skips the key entirely so Graph keeps
# its default rather than receiving an empty/zero value.
function Get-BulkAssignmentSettingValue
{
param($Form, $Field)
$ctl = $Form.FindName($Field.Control)
if(-not $ctl) { return $null }
switch ($Field.Kind) {
"bool" { return [bool]$ctl.IsChecked }
"string" {
$v = [string]$ctl.Text
if([string]::IsNullOrWhiteSpace($v)) { return $null }
return $v
}
"int" {
$v = [string]$ctl.Text
if([string]::IsNullOrWhiteSpace($v)) { return $null }
$parsed = 0
if([int]::TryParse($v, [ref]$parsed)) { return $parsed }
return $null
}
"enum" {
$v = [string]$ctl.SelectedItem
if([string]::IsNullOrWhiteSpace($v)) { return $null }
return $v
}
"datetime" {
$v = [string]$ctl.Text
if([string]::IsNullOrWhiteSpace($v)) { return $null }
return $v # Graph accepts ISO8601 as a string in the JSON body
}
"stringList" {
$v = [string]$ctl.Text
if([string]::IsNullOrWhiteSpace($v)) { return $null }
$items = @($v -split "[`r`n;,]+" | ForEach-Object { $_.Trim() } | Where-Object { $_ })
if($items.Count -eq 0) { return $null }
return ,$items
}
}
return $null
}
# Push a stored hashtable value back into the UI control. Inverse of
# Get-BulkAssignmentSettingValue. Tolerates missing keys (control left at
# its XAML-default state).
function Set-BulkAssignmentSettingValue
{
param($Form, $Field, $Value)
$ctl = $Form.FindName($Field.Control)
if(-not $ctl) { return }
switch ($Field.Kind) {
"bool" { if($null -ne $Value) { $ctl.IsChecked = [bool]$Value } }
"string" { if($null -ne $Value) { $ctl.Text = [string]$Value } }
"int" { if($null -ne $Value) { $ctl.Text = [string]$Value } }
"enum" { if($null -ne $Value) { $ctl.SelectedItem = [string]$Value } }
"datetime" { if($null -ne $Value) { $ctl.Text = [string]$Value } }
"stringList" {
if($null -ne $Value) {
$ctl.Text = (@($Value) -join [Environment]::NewLine)
}
}
}
}
function Show-BulkAssignmentSettingsDialog
{
param([Parameter(Mandatory)]$Row)
$form = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkAssignmentsSettings.xaml"))
if(-not $form) { return }
$script:_bulkAssignSettingsForm = $form
$script:_bulkAssignSettingsRow = $Row
# Populate enum combos once. Lists come from the Graph CSDL — keep them
# in lock-step with the metadata when Microsoft adds new enum members.
$enumLists = @{
cbAndroidStore_AutoUpdate = @("default","postponed","priority")
cbWin32_DeliveryOpt = @("notConfigured","foreground")
cbWin32_Notifications = @("showAll","showReboot","hideAll")
cbWin32_Superseded = @("notConfigured","enabled")
cbWinGet_Notifications = @("showAll","showReboot","hideAll")
cbWinAutoUpdate_DeliveryOpt = @("notConfigured","foreground")
cbWinAutoUpdate_Notifications = @("showAll","showReboot","hideAll")
cbScript_ScheduleType = @("Hourly","Daily","Once")
}
foreach($name in $enumLists.Keys) {
$ctl = $form.FindName($name)
if($ctl) { $ctl.ItemsSource = $enumLists[$name] }
}
# Custom load: Health Script tab uses a polymorphic runSchedule whose
# @odata.type depends on ScheduleType. Stored under the synthetic
# "deviceHealthScriptAssignment" key in Row.Settings (not a Graph type
# name — just an internal marker the public command consumes).
if($Row.Settings -and $Row.Settings.ContainsKey('deviceHealthScriptAssignment')) {
$hs = $Row.Settings['deviceHealthScriptAssignment']
if($hs) {
$form.FindName('chkApplyHealthScript').IsChecked = $true
if($hs.ContainsKey('runRemediationScript')) {
$form.FindName('chkScript_RunRemediation').IsChecked = [bool]$hs['runRemediationScript']
}
if($hs.ContainsKey('scheduleType')) { $form.FindName('cbScript_ScheduleType').SelectedItem = [string]$hs['scheduleType'] }
if($hs.ContainsKey('interval')) { $form.FindName('txtScript_Interval').Text = [string]$hs['interval'] }
if($hs.ContainsKey('time')) { $form.FindName('txtScript_Time').Text = [string]$hs['time'] }
if($hs.ContainsKey('useUtc')) { $form.FindName('chkScript_UseUtc').IsChecked = [bool]$hs['useUtc'] }
if($hs.ContainsKey('date')) { $form.FindName('txtScript_Date').Text = [string]$hs['date'] }
}
}
# Load existing values from Row.Settings into the controls.
foreach($tab in $script:_bulkAssignSettingsSchema) {
$existing = $null
if($Row.Settings -and $Row.Settings.ContainsKey($tab.Type)) {
$existing = $Row.Settings[$tab.Type]
}
if(-not $existing) { continue }
$applyCtl = $form.FindName($tab.Apply)
if($applyCtl) { $applyCtl.IsChecked = $true }
foreach($field in $tab.Fields) {
if($existing.ContainsKey($field.Key)) {
Set-BulkAssignmentSettingValue $form $field $existing[$field.Key]
}
}
if($tab.Nested) {
foreach($nested in $tab.Nested) {
if(-not $existing.ContainsKey($nested.Key)) { continue }
$nestedHash = $existing[$nested.Key]
if(-not $nestedHash) { continue }
foreach($field in $nested.Fields) {
if($nestedHash.ContainsKey($field.Key)) {
Set-BulkAssignmentSettingValue $form $field $nestedHash[$field.Key]
}
}
}
}
}
$script:UIProvider.AddXamlEvent($form, "btnBulkAssignSettingsOK", "add_click", {
$r = $script:_bulkAssignSettingsRow
$f = $script:_bulkAssignSettingsForm
$new = @{}
foreach($tab in $script:_bulkAssignSettingsSchema) {
$applyCtl = $f.FindName($tab.Apply)
if(-not $applyCtl -or -not $applyCtl.IsChecked) { continue }
$hash = @{}
foreach($field in $tab.Fields) {
$v = Get-BulkAssignmentSettingValue $f $field
if($null -ne $v) { $hash[$field.Key] = $v }
}
if($tab.Nested) {
foreach($nested in $tab.Nested) {
$nestedHash = @{}
foreach($field in $nested.Fields) {
$v = Get-BulkAssignmentSettingValue $f $field
if($null -ne $v) { $nestedHash[$field.Key] = $v }
}
if($nestedHash.Count -gt 0) {
$nestedHash["@odata.type"] = "#microsoft.graph.$($nested.Type)"
$hash[$nested.Key] = $nestedHash
}
}
}
# Only include the tab if SOMETHING is set. Otherwise it's just
# the apply-checkbox ticked with all defaults — no value to send.
if($hash.Count -gt 0) { $new[$tab.Type] = $hash }
}
# Custom save: Health Script tab. Stored as a flat hashtable; the
# public command picks the schedule @odata.type at POST time from
# the scheduleType value here.
$applyHs = $f.FindName('chkApplyHealthScript')
if($applyHs -and $applyHs.IsChecked) {
$hsHash = @{
runRemediationScript = [bool]$f.FindName('chkScript_RunRemediation').IsChecked
}
$st = [string]$f.FindName('cbScript_ScheduleType').SelectedItem
if($st) {
$hsHash['scheduleType'] = $st
$intervalText = [string]$f.FindName('txtScript_Interval').Text
$parsedInterval = 0
if(-not [string]::IsNullOrWhiteSpace($intervalText) -and [int]::TryParse($intervalText, [ref]$parsedInterval)) {
$hsHash['interval'] = $parsedInterval
}
if($st -in @('Daily','Once')) {
$timeText = [string]$f.FindName('txtScript_Time').Text
if(-not [string]::IsNullOrWhiteSpace($timeText)) { $hsHash['time'] = $timeText.Trim() }
$hsHash['useUtc'] = [bool]$f.FindName('chkScript_UseUtc').IsChecked
}
if($st -eq 'Once') {
$dateText = [string]$f.FindName('txtScript_Date').Text
if(-not [string]::IsNullOrWhiteSpace($dateText)) { $hsHash['date'] = $dateText.Trim() }
}
}
$new['deviceHealthScriptAssignment'] = $hsHash
}
$r.Settings = $new
$r.SettingsDisplay = if($new.Count -gt 0) { "$($new.Count) platform(s)" } else { "(none)" }
try { $script:dgBulkAssignList.Items.Refresh() } catch { }
$script:_bulkAssignSettingsForm = $null
$script:_bulkAssignSettingsRow = $null
Close-TopModalObject
})
$script:UIProvider.AddXamlEvent($form, "btnBulkAssignSettingsCancel", "add_click", {
$script:_bulkAssignSettingsForm = $null
$script:_bulkAssignSettingsRow = $null
Close-TopModalObject
})
$script:UIProvider.ShowModalForm("App settings - $($Row.GroupName)", $form, $true)
}
# Group-picker dialog. Searches AAD groups via Graph (startsWith), lets the
# user pick include/exclude direction and optionally attach a deviceAnd
# AppManagementAssignmentFilter. Reuses the global modal scaffolding so the
# parent Bulk Assignments form stays put underneath.
function Show-BulkAssignmentGroupPicker
{
$picker = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkAssignmentsGroupPicker.xaml"))
if(-not $picker) { return }
$script:_bulkAssignGroupPicker = $picker
$txtSearch = $picker.FindName("txtGroupSearch")
$btnSearch = $picker.FindName("btnGroupSearch")
$lstResults = $picker.FindName("lstGroupResults")
$cbFilter = $picker.FindName("cbGroupFilter")
$cbIntent = $picker.FindName("cbGroupIntent")
if(-not $txtSearch -or -not $btnSearch -or -not $lstResults -or -not $cbFilter -or -not $cbIntent) {
Write-Log "Bulk Assignments group picker XAML loaded, but required controls were not found" 3
return
}
# Populate assignment filters once. Empty entry = no filter.
$tokenId = Get-DefaultTokenId
$filters = @([PSCustomObject]@{ Id = $null; Name = "(no filter)" })
try {
$resp = Invoke-MSGraphAPI -Url "deviceManagement/assignmentFilters" -TokenId $tokenId -AllPages
if($resp -and $resp.value) {
foreach($f in @($resp.value | Sort-Object displayName)) {
$filters += [PSCustomObject]@{ Id = [string]$f.id; Name = [string]$f.displayName }
}
}
}
catch { Write-LogDebug "Bulk Assignments: failed to load assignment filters: $($_.Exception.Message)" }
$cbFilter.ItemsSource = $filters
$cbFilter.SelectedIndex = 0
# installIntent values from Graph's mobileAppAssignment schema. Required
# is the most common bulk action so make it the default.
$intents = @(
[PSCustomObject]@{ Name = "Required"; Value = "required" }
[PSCustomObject]@{ Name = "Available"; Value = "available" }
[PSCustomObject]@{ Name = "Uninstall"; Value = "uninstall" }
[PSCustomObject]@{ Name = "Available without enrollment"; Value = "availableWithoutEnrollment" }
[PSCustomObject]@{ Name = "Not available (hidden)"; Value = "notAvailable" }
)
$cbIntent.ItemsSource = $intents
$cbIntent.SelectedValue = "required"
$searchAction = {
$activePicker = $script:_bulkAssignGroupPicker
$searchBox = if($activePicker) { $activePicker.FindName("txtGroupSearch") } else { $null }
$resultsList = if($activePicker) { $activePicker.FindName("lstGroupResults") } else { $null }
if(-not $searchBox -or -not $resultsList -or -not ($resultsList -is [System.Windows.Controls.ItemsControl])) {
Write-Log "Bulk Assignments: group search controls were not available" 3
return
}
$term = [string]$searchBox.Text
if($term.Length -lt 1) {
$resultsList.ItemsSource = $null
return
}
$escaped = $term.Replace("'", "''")
$url = "groups?`$top=25&`$select=id,displayName&`$filter=startswith(displayName,'$escaped')"
try {
$resp = Invoke-MSGraphAPI -Url $url -TokenId (Get-DefaultTokenId)
if($resp -and $resp.value) {
$resultsList.ItemsSource = @($resp.value | Sort-Object displayName)
}
else {
$resultsList.ItemsSource = @()
}
}
catch {
Write-LogError "Bulk Assignments: group search failed" $_.Exception
$resultsList.ItemsSource = @()
}
}
$btnSearch.Add_Click($searchAction)
$txtSearch.Add_KeyDown({
param($src, $e)
if($e.Key -eq "Return") { & $searchAction }
})
$script:UIProvider.AddXamlEvent($picker, "btnGroupPickerOK", "add_click", {
$sel = $script:_bulkAssignGroupPicker.FindName("lstGroupResults").SelectedItem
if(-not $sel) {
$script:UIProvider.ShowMessageBox("Pick a group from the search results first.", "Bulk Assignments", "OK", "Warning") | Out-Null
return
}
$isExclude = [bool]$script:_bulkAssignGroupPicker.FindName("rbGroupExclude").IsChecked
$targetType = if($isExclude) { "exclusionGroupAssignmentTarget" } else { "groupAssignmentTarget" }
$filterItem = $script:_bulkAssignGroupPicker.FindName("cbGroupFilter").SelectedItem
$filterId = if($filterItem) { [string]$filterItem.Id } else { $null }
$filterName = if($filterItem -and $filterId) { [string]$filterItem.Name } else { $null }
$filterType = "include"
if($filterId -and [bool]$script:_bulkAssignGroupPicker.FindName("rbGroupFilterExclude").IsChecked) {
$filterType = "exclude"
}
$intent = [string]$script:_bulkAssignGroupPicker.FindName("cbGroupIntent").SelectedValue
if(-not $intent) { $intent = "required" }
Add-BulkAssignmentRow `
-TargetType $targetType `
-GroupId ([string]$sel.id) `
-GroupName ([string]$sel.displayName) `
-FilterId $filterId `
-FilterName $filterName `
-FilterType $filterType `
-Intent $intent
$script:_bulkAssignGroupPicker = $null
Close-TopModalObject
})
$script:UIProvider.AddXamlEvent($picker, "btnGroupPickerCancel", "add_click", {
$script:_bulkAssignGroupPicker = $null
Close-TopModalObject
})
$script:UIProvider.ShowModalForm("Pick a group", $picker, $true)
}
@@ -0,0 +1,100 @@
# Bulk Copy form (ported from the original project's Copy extension).
# Thin WPF wrapper around the public Start-GraphBulkCopy driver — the UI only
# collects the two name patterns and the selected policy types (R10).
function Show-GraphBulkCopyForm
{
$script:bulkCopyForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkCopy.xaml"), $true)
if(-not $script:bulkCopyForm) { return }
$script:dgBulkCopyObjects = $script:bulkCopyForm.FindName("dgBulkCopyObjects")
$script:UIProvider.SetXamlProperty($script:bulkCopyForm, "txtCopyFromPattern", "Text", (Get-SettingStoreValue "Copy" "CopyFromPattern"))
$script:UIProvider.SetXamlProperty($script:bulkCopyForm, "txtCopyToPattern", "Text", (Get-SettingStoreValue "Copy" "CopyToPattern"))
# Rows are policy TYPES (matching the original Bulk Copy, which listed every
# type in the active view), all selected by default.
$script:copyObjects = @()
foreach($intuneType in $script:IntuneTypes)
{
if(-not $intuneType.Title) { continue }
if($intuneType.ShowButtons -is [Object[]] -and $intuneType.ShowButtons -notcontains "Copy") { continue }
$script:copyObjects += New-Object PSObject -Property @{
Title = $intuneType.Title
Selected = $true
ObjectType = $intuneType
}
}
$script:copyObjects = @($script:copyObjects | Sort-Object -Property Title)
$column = Get-GridCheckboxColumn "Selected"
$script:dgBulkCopyObjects.Columns.Add($column)
$column.Header.IsChecked = $true
$column.Header.add_Click({
foreach($Item in $script:dgBulkCopyObjects.ItemsSource)
{
$Item.Selected = $this.IsChecked
}
$script:dgBulkCopyObjects.Items.Refresh()
})
$binding = [System.Windows.Data.Binding]::new("Title")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Object type"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:dgBulkCopyObjects.Columns.Add($column)
$script:dgBulkCopyObjects.ItemsSource = $script:copyObjects
$script:UIProvider.AddXamlEvent($script:bulkCopyForm, "btnClose", "add_click", {
$script:bulkCopyForm = $null
Show-ModalObject
})
$script:UIProvider.AddXamlEvent($script:bulkCopyForm, "btnStartCopy", "add_click", {
$copyFrom = ($script:UIProvider.GetXamlProperty($script:bulkCopyForm, "txtCopyFromPattern", "Text")).Trim()
$copyTo = ($script:UIProvider.GetXamlProperty($script:bulkCopyForm, "txtCopyToPattern", "Text")).Trim()
if(-not $copyFrom -or -not $copyTo)
{
$script:UIProvider.ShowMessageBox("Both name patterns must be specified", "Error", "OK", "Error")
return
}
$selectedTypes = @($script:copyObjects | Where-Object Selected -eq $true)
if($selectedTypes.Count -eq 0)
{
$script:UIProvider.ShowMessageBox("No object types selected.`n`nSelect the types you want to copy.", "Error", "OK", "Error")
return
}
Save-SettingStoreValue "Copy" "CopyFromPattern" $copyFrom
Save-SettingStoreValue "Copy" "CopyToPattern" $copyTo
Write-Status "Copy objects" -Block
try
{
$summary = Start-GraphBulkCopy -CopyFromPattern $copyFrom -CopyToPattern $copyTo `
-PolicyType @($selectedTypes | ForEach-Object { $_.ObjectType.Id })
Write-Status $null
$msg = ("Copied {0} object(s) across {1} type(s) ({2} skipped because the target name exists) in {3:hh\:mm\:ss}." -f `
$summary.Copied, $summary.Types, $summary.Skipped, $summary.Duration)
$unknownNote = Get-IntuneUnknownSelectorSummary $summary.UnknownSelectors
if($unknownNote) { $msg += "`n`n$unknownNote" }
$script:UIProvider.ShowMessageBox($msg, "Bulk Copy", "OK", "Information") | Out-Null
}
catch
{
Write-Status $null
Write-LogError "Bulk copy failed" $_.Exception
$script:UIProvider.ShowMessageBox("Bulk copy failed:`n`n$($_.Exception.Message)", "Bulk Copy", "OK", "Error") | Out-Null
}
# Refresh the current view so any copies in the active type show up.
Invoke-IntuneActivateObject $script:IntuneManagerSelectedObject | Out-Null
})
$script:UIProvider.ShowModalForm("Bulk Copy Objects", $script:bulkCopyForm, $true)
}
@@ -0,0 +1,104 @@
# Bulk Delete form.
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Show-GraphBulkDeleteForm
{
$script:bulkDeleteForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkDeleteForm.xaml"), $true)
if(-not $script:bulkDeleteForm) { return }
$script:dgBulkDeleteObjects = $script:bulkDeleteForm.FindName("dgBulkDeleteObjects")
$script:UIProvider.SetXamlProperty($script:bulkDeleteForm, "txtDeleteNameFilter", "Text", (Get-SettingStoreValue "" "DeleteNameFilter"))
$script:deleteObjects = @()
foreach($intuneGroup in $script:IntuneGroups)
{
if(-not $intuneGroup.Title) { continue }
if($intuneGroup.ShowButtons -is [Object[]] -and $intuneGroup.ShowButtons -notcontains "Delete") { continue }
$script:deleteObjects += New-Object PSObject -Property @{
Title = $intuneGroup.Title
Selected = $false
ObjectGroup = $intuneGroup
}
}
$column = Get-GridCheckboxColumn "Selected"
$script:dgBulkDeleteObjects.Columns.Add($column)
$column.Header.IsChecked = $false
$column.Header.add_Click({
foreach($Item in $script:dgBulkDeleteObjects.ItemsSource)
{
$Item.Selected = $this.IsChecked
}
$script:dgBulkDeleteObjects.Items.Refresh()
})
$binding = [System.Windows.Data.Binding]::new("Title")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Object type"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:dgBulkDeleteObjects.Columns.Add($column)
$script:dgBulkDeleteObjects.ItemsSource = $script:deleteObjects
$script:UIProvider.AddXamlEvent($script:bulkDeleteForm, "btnClose", "add_click", {
$script:bulkDeleteForm = $null
Show-ModalObject
})
$script:UIProvider.AddXamlEvent($script:bulkDeleteForm, "btnDelete", "add_click", {
$selectedGroups = @($script:deleteObjects | Where-Object Selected -eq $true)
if($selectedGroups.Count -eq 0)
{
$script:UIProvider.ShowMessageBox("No object types selected.`n`nSelect the types you want to delete.", "Error", "OK", "Error")
return
}
$nameFilter = ($script:UIProvider.GetXamlProperty($script:bulkDeleteForm, "txtDeleteNameFilter", "Text")).Trim()
Save-SettingStoreValue "" "DeleteNameFilter" $nameFilter
$confirmMsg = "Are you sure you want to delete all objects of the selected type(s)?`n`n$($selectedGroups.Count) type(s) selected`n`nEnvironment: $script:OrganizationName"
if($nameFilter) { $confirmMsg += "`nName filter: $nameFilter" }
if(($script:UIProvider.ShowMessageBox($confirmMsg, "Delete Objects?", "YesNo", "Warning")) -ne "Yes")
{
return
}
# The UI is a thin caller of the public driver (R10) — the confirm
# prompt above is the UI's job; the delete loop itself runs headless
# via Start-GraphBulkDelete.
Write-Status "Bulk delete" -Block
try
{
$summary = Start-GraphBulkDelete -Filter $nameFilter `
-PolicyGroup @($selectedGroups | ForEach-Object { $_.ObjectGroup.ID })
# Delete shows nothing on plain success; an id that matched nothing is
# the one outcome the user must not miss.
$unknownNote = Get-IntuneUnknownSelectorSummary $summary.UnknownSelectors
if($unknownNote) {
$script:UIProvider.ShowMessageBox($unknownNote, "Bulk Delete", "OK", "Warning") | Out-Null
}
}
catch
{
Write-LogError "Bulk delete failed" $_.Exception
$script:UIProvider.ShowMessageBox("Bulk delete failed:`n`n$($_.Exception.Message)", "Bulk Delete", "OK", "Error") | Out-Null
}
Invoke-IntuneActivateObject $script:IntuneManagerSelectedObject | Out-Null
Write-Status ""
})
$script:UIProvider.ShowModalForm("Bulk Delete", $script:bulkDeleteForm, $true)
}
#region Bulk Documentation
# Thin WPF wrapper around Start-GraphBulkDocumentation. The same public driver
# powers the silent batch path (Tests/Setup/Invoke-AutomationOrchestrator) so
# every code path goes through the same engine.
@@ -0,0 +1,495 @@
# Bulk Documentation form + per-view Show-IntuneManagerDocumentForm caller.
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Set-WpfDocumentationCombo {
param($Form, [string]$Name, $Items, $SelectedValue)
$control = $Form.FindName($Name)
if (-not $control) { return }
$control.ItemsSource = Get-UIItemsArray $Items
if ($null -ne $SelectedValue) { $control.SelectedValue = $SelectedValue }
if ($control.SelectedIndex -lt 0 -and $control.Items.Count -gt 0) { $control.SelectedIndex = 0 }
}
# Show/hide $TargetNames whenever $TriggerName's SelectedValue matches
# $VisibleWhen. Mirrors the OLD project's per-output-form selectionChanged
# wiring (DocumentationWord.psm1 ~L126, Documentation.psm1 ~L5029) so the
# Custom Properties row only appears for Output Properties = "custom" and
# the limit-length grid only appears for Output Level = "limited".
function Set-WpfDocumentationConditionalVisibility {
param(
$Form,
[string]$TriggerName,
[string]$VisibleWhen,
[string[]]$TargetNames
)
$trigger = $Form.FindName($TriggerName)
if (-not $trigger) { return }
$targets = @($TargetNames | ForEach-Object { $Form.FindName($_) } | Where-Object { $_ })
if ($targets.Count -eq 0) { return }
$apply = {
param($selectedValue)
$vis = if ([string]$selectedValue -eq $VisibleWhen) { 'Visible' } else { 'Collapsed' }
foreach ($t in $targets) { $t.Visibility = $vis }
}.GetNewClosure()
& $apply $trigger.SelectedValue
$trigger.Add_SelectionChanged({
param($s, $e)
& $apply $s.SelectedValue
}.GetNewClosure())
}
function Initialize-WpfDocumentationOptions {
param($Form)
Set-WpfDocumentationCombo $Form 'cbDocumentationLanguage' (Get-DocumentationLanguages | Sort-Object EnglishName) (Get-DocumentationSetting 'Language' 'en')
Set-WpfDocumentationCombo $Form 'cbDocumentationPropertySeparator' @(',',';','-','|') (Get-DocumentationSetting 'PropertySeparator' ';')
Set-WpfDocumentationCombo $Form 'cbDocumentationObjectSeparator' @(
[PSCustomObject]@{ Name='New line'; Value=[Environment]::NewLine },
[PSCustomObject]@{ Name=';'; Value=';' },
[PSCustomObject]@{ Name='|'; Value='|' }
) (Get-DocumentationSetting 'ObjectSeparator' ([Environment]::NewLine))
Set-WpfDocumentationCombo $Form 'cbNotConfiguredText' @(
[PSCustomObject]@{Name='Not configured (localized)';Value='notConfigured'},
[PSCustomObject]@{Name='Empty';Value='empty'},
[PSCustomObject]@{Name="Don't change";Value='asis'}
) (Get-DocumentationSetting 'NotConfiguredText' 'notConfigured')
Set-WpfDocumentationCombo $Form 'cbValueOutputProperty' @(
[PSCustomObject]@{Name='Value';Value='value'},
[PSCustomObject]@{Name='Value with label';Value='valueWithLabel'}
) (Get-DocumentationSetting 'ValueOutputProperty' 'value')
$propertyModes = @([PSCustomObject]@{Name='Simple';Value='simple'},[PSCustomObject]@{Name='Extended';Value='extended'},[PSCustomObject]@{Name='Custom';Value='custom'})
$fileModes = @([PSCustomObject]@{Name='Single file';Value='Full'},[PSCustomObject]@{Name='One file per object';Value='Object'})
Set-WpfDocumentationCombo $Form 'cbCSVDocumentationProperties' $propertyModes (Get-DocumentationSetting 'CSVExportProperties' 'simple')
Set-WpfDocumentationCombo $Form 'cbCSVDelimiter' @('',',',';','-','|') (Get-DocumentationSetting 'CSVDelimiter' '')
Set-WpfDocumentationCombo $Form 'cbJsonOutputFileType' @([PSCustomObject]@{Name='Single file';Value='Full'},[PSCustomObject]@{Name='One file per object type';Value='ObjectType'}) (Get-DocumentationSetting 'JSONOutputFileType' 'Full')
Set-WpfDocumentationCombo $Form 'cbHTMLDocumentFileType' $fileModes (Get-DocumentationSetting 'HTMLDocumentFileType' 'Full')
Set-WpfDocumentationCombo $Form 'cbAtlassianDocumentFileType' $fileModes (Get-DocumentationSetting 'AtlassianDocumentFileType' 'Full')
Set-WpfDocumentationCombo $Form 'cbMDDocumentFileType' $fileModes (Get-DocumentationSetting 'MDDocumentFileType' 'Full')
Set-WpfDocumentationCombo $Form 'cbWordExportProperties' $propertyModes (Get-DocumentationSetting 'WordExportProperties' 'simple')
Set-WpfDocumentationCombo $Form 'cbWordDocumentFormat' @([PSCustomObject]@{Name='DOCX';Value='wdFormatDocumentDefault'},[PSCustomObject]@{Name='Strict Open XML';Value='wdFormatStrictOpenXMLDocument'},[PSCustomObject]@{Name='PDF';Value='wdFormatPDF'}) (Get-DocumentationSetting 'WordDocumentFormat' 'wdFormatDocumentDefault')
Set-WpfDocumentationCombo $Form 'cbWordDocumentationLevel' @([PSCustomObject]@{Name='Full';Value='full'},[PSCustomObject]@{Name='Limited';Value='limited'},[PSCustomObject]@{Name='Basic';Value='basic'}) (Get-DocumentationSetting 'WordDocumentationLevel' 'full')
Set-WpfDocumentationCombo $Form 'cbWordTableCaptionPosition' @([PSCustomObject]@{Name='Below table';Value='below'},[PSCustomObject]@{Name='Above table';Value='above'}) (Get-DocumentationSetting 'WordTableCaptionPosition' 'below')
$defaults = @{
txtCSVDocumentationPath='CSVDocumentationPath'; txtCSVCustomProperties='CSVCustomDisplayProperties'; txtJsonDocumentName='JSONDocumentName'
txtHTMLDocumentName='HTMLDocumentName'; txtHTMLCSSFile='HTMLCSSFile'; txtHTMLTitleProperty='HTMLTitleProperty'
txtAtlassianDocumentName='AtlassianDocumentName'; txtAtlassianTitleProperty='AtlassianTitleProperty'
txtMDDocumentName='MDDocumentName'; txtMDCSSFile='MDCSSFile'; txtMDTitleProperty='MDTitleProperty'
txtWordDocumentName='WordDocumentName'; txtWordDocumentTemplate='WordDocumentTemplate'; txtWordCustomDisplayProperties='WordCustomDisplayProperties'
txtWordDocumentationLimitMaxLength='WordDocumentationLimitMaxLength'; txtWordDocumentationLimitTruncateLength='WordDocumentationLimitTruncateLength'
txtWordTitleProperty='WordTitleProperty'; txtWordSubjectProperty='WordSubjectProperty'; txtWordContentControls='WordContentControls'
txtWordCoverPage='WordCoverPage'; txtWordHeader1Style='WordHeader1Style'; txtWordHeader2Style='WordHeader2Style'; txtWordHeader3Style='WordHeader3Style'
txtWordTableStyle='WordTableStyle'; txtWordTableHeaderStyle='WordTableHeaderStyle'; txtWordCategoryHeaderStyle='WordCategoryHeaderStyle'
txtWordSubCategoryHeaderStyle='WordSubCategoryHeaderStyle'; txtWordTableTextStyle='WordTableTextStyle'
txtWordScriptTableStyle='WordScriptTableStyle'; txtWordScriptStyle='WordScriptStyle'
}
foreach($name in $defaults.Keys) {
$c = $Form.FindName($name)
# Some output XAML files don't carry every control yet (e.g. the
# WPF MD form omits TitleProperty); skip silently rather than
# NullReferenceException-on-property-set.
if ($c) { $c.Text = [string](Get-DocumentationSetting $defaults[$name] '') }
}
foreach($pair in @(
@('chkSkipNotConfigured','SkipNotConfigured',$false),@('chkSkipDefaultValues','SkipDefaultValues',$false),@('chkSkipDisabled','SkipDisabled',$true),
@('chkSetUnconfiguredValue','SetUnconfiguredValue',$true),@('chkSetDefaultValue','SetDefaultValue',$false),@('chkIncludeScripts','IncludeScripts',$true),
@('chkExcludeScriptSignature','ExcludeScriptSignature',$false),@('chkExcludeAssignments','ExcludeAssignments',$false),@('chkIncludePolicyId','IncludePolicyId',$false),@('chkSkipDocumentInfo','SkipDocumentInfo',$false),@('chkFallbackDocumentation','FallbackDocumentation',$true),
@('chkCSVAddObjectType','CSVAddObjectType',$true),@('chkCSVAddCompanyName','CSVAddCompanyName',$false),@('chkJsonOpenFile','JSONOpenFile',$true),
@('chkHTMLOpenFile','HTMLOpenFile',$true),@('chkAtlassianOpenFile','AtlassianOpenFile',$true),
@('chkMDIncludeCSS','MDIncludeCSS',$true),@('chkMDOpenFile','MDOpenFile',$true),@('chkMDDocumentSkipDate','MDDocumentSkipDate',$false),
@('chkWordDocumentationLimitAttach','WordDocumentationLimitAttach',$false),@('chkWordAddCategories','WordAddCategories',$true),@('chkWordAddSubCategories','WordAddSubCategories',$true),
@('chkWordOpenDocument','WordOpenDocument',$true),@('chkWordAttachJsonFile','WordAttachJsonFile',$false)
)) {
$c = $Form.FindName($pair[0])
if ($c) { $c.IsChecked = (Get-DocumentationSetting $pair[1] $pair[2]) -eq $true }
}
# Conditional-visibility wiring: matches the OLD project's per-output-form
# selectionChanged handlers. The Custom Properties row only appears for
# Output Properties = "custom"; the limit-length grid only appears for
# Output Level = "limited".
Set-WpfDocumentationConditionalVisibility -Form $Form -TriggerName 'cbWordExportProperties' -VisibleWhen 'custom' `
-TargetNames @('spWordCustomProperties','txtWordCustomDisplayProperties')
Set-WpfDocumentationConditionalVisibility -Form $Form -TriggerName 'cbWordDocumentationLevel' -VisibleWhen 'limited' `
-TargetNames @('gdWordDocumentationLimitOptions')
Set-WpfDocumentationConditionalVisibility -Form $Form -TriggerName 'cbCSVDocumentationProperties' -VisibleWhen 'custom' `
-TargetNames @('spCSVCustomProperties','txtCSVCustomProperties')
}
function Get-WpfDocumentationOptions {
param($Form)
# Missing-control safety: if a XAML file doesn't carry a referenced
# control (e.g. legacy form without the new field), return $null/empty
# rather than tripping NullReferenceException. Save-DocumentationOptionsDefaults
# later filters $null so we don't overwrite the user's persisted setting
# with garbage on click-Export.
function Txt($n) { $c = $Form.FindName($n); if ($c) { [string]$c.Text } else { $null } }
function Sel($n) { $c = $Form.FindName($n); if ($c) { $c.SelectedValue } else { $null } }
function Chk($n) { $c = $Form.FindName($n); if ($c) { [bool]$c.IsChecked } else { $null } }
@{
Language=Sel 'cbDocumentationLanguage'; PropertySeparator=Sel 'cbDocumentationPropertySeparator'; ObjectSeparator=Sel 'cbDocumentationObjectSeparator'; NameFilter=Txt 'txtDocumentFilter'; SourceFolder=Txt 'txtDocumentFromFolder'
SkipNotConfigured=Chk 'chkSkipNotConfigured'; SkipDefaultValues=Chk 'chkSkipDefaultValues'; SkipDisabled=Chk 'chkSkipDisabled'
SetUnconfiguredValue=Chk 'chkSetUnconfiguredValue'; SetDefaultValue=Chk 'chkSetDefaultValue'; IncludeScripts=Chk 'chkIncludeScripts'
ExcludeScriptSignature=Chk 'chkExcludeScriptSignature'; ExcludeAssignments=Chk 'chkExcludeAssignments'; IncludePolicyId=Chk 'chkIncludePolicyId'
SkipDocumentInfo=Chk 'chkSkipDocumentInfo'; FallbackDocumentation=Chk 'chkFallbackDocumentation'
NotConfiguredText=Sel 'cbNotConfiguredText'; ValueOutputProperty=Sel 'cbValueOutputProperty'
Outputs=@{
csv=@{CSVDocumentationPath=Txt 'txtCSVDocumentationPath';CSVExportProperties=Sel 'cbCSVDocumentationProperties';CSVCustomDisplayProperties=Txt 'txtCSVCustomProperties';CSVDelimiter=Sel 'cbCSVDelimiter';CSVAddObjectType=Chk 'chkCSVAddObjectType';CSVAddCompanyName=Chk 'chkCSVAddCompanyName'}
json=@{JSONDocumentName=Txt 'txtJsonDocumentName';JSONOpenFile=Chk 'chkJsonOpenFile';JSONOutputFileType=Sel 'cbJsonOutputFileType'}
html=@{HTMLDocumentName=Txt 'txtHTMLDocumentName';HTMLCSSFile=Txt 'txtHTMLCSSFile';HTMLTitleProperty=Txt 'txtHTMLTitleProperty';HTMLOpenFile=Chk 'chkHTMLOpenFile';HTMLDocumentFileType=Sel 'cbHTMLDocumentFileType'}
atlassian=@{AtlassianDocumentName=Txt 'txtAtlassianDocumentName';AtlassianTitleProperty=Txt 'txtAtlassianTitleProperty';AtlassianOpenFile=Chk 'chkAtlassianOpenFile';AtlassianDocumentFileType=Sel 'cbAtlassianDocumentFileType'}
md=@{MDDocumentName=Txt 'txtMDDocumentName';MDCSSFile=Txt 'txtMDCSSFile';MDTitleProperty=Txt 'txtMDTitleProperty';MDIncludeCSS=Chk 'chkMDIncludeCSS';MDOpenFile=Chk 'chkMDOpenFile';MDDocumentSkipDate=Chk 'chkMDDocumentSkipDate';MDDocumentFileType=Sel 'cbMDDocumentFileType'}
word=@{WordDocumentName=Txt 'txtWordDocumentName';WordDocumentTemplate=Txt 'txtWordDocumentTemplate';WordExportProperties=Sel 'cbWordExportProperties';WordCustomDisplayProperties=Txt 'txtWordCustomDisplayProperties';WordDocumentFormat=Sel 'cbWordDocumentFormat';WordDocumentationLevel=Sel 'cbWordDocumentationLevel';WordDocumentationLimitMaxLength=Txt 'txtWordDocumentationLimitMaxLength';WordDocumentationLimitTruncateLength=Txt 'txtWordDocumentationLimitTruncateLength';WordDocumentationLimitAttach=Chk 'chkWordDocumentationLimitAttach';WordAddCategories=Chk 'chkWordAddCategories';WordAddSubCategories=Chk 'chkWordAddSubCategories';WordOpenDocument=Chk 'chkWordOpenDocument';WordAttachJsonFile=Chk 'chkWordAttachJsonFile';WordTitleProperty=Txt 'txtWordTitleProperty';WordSubjectProperty=Txt 'txtWordSubjectProperty';WordContentControls=Txt 'txtWordContentControls';WordCoverPage=Txt 'txtWordCoverPage';WordHeader1Style=Txt 'txtWordHeader1Style';WordHeader2Style=Txt 'txtWordHeader2Style';WordHeader3Style=Txt 'txtWordHeader3Style';WordTableStyle=Txt 'txtWordTableStyle';WordTableHeaderStyle=Txt 'txtWordTableHeaderStyle';WordCategoryHeaderStyle=Txt 'txtWordCategoryHeaderStyle';WordSubCategoryHeaderStyle=Txt 'txtWordSubCategoryHeaderStyle';WordTableTextStyle=Txt 'txtWordTableTextStyle';WordTableCaptionPosition=Sel 'cbWordTableCaptionPosition';WordScriptTableStyle=Txt 'txtWordScriptTableStyle';WordScriptStyle=Txt 'txtWordScriptStyle'}
}
}
}
function Add-WpfDocumentationFileBrowse {
param($Form, [string]$ButtonName, [string]$TextName, [switch]$Save, [string]$Filter = 'All files (*.*)|*.*')
$script:UIProvider.AddXamlEvent($Form, $ButtonName, 'add_click', {
$dialog = if($Save) { [System.Windows.Forms.SaveFileDialog]::new() } else { [System.Windows.Forms.OpenFileDialog]::new() }
$dialog.Filter = $Filter
$current = [string]$Form.FindName($TextName).Text
if($current) { $dialog.FileName = $current }
if($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$Form.FindName($TextName).Text = $dialog.FileName
}
}.GetNewClosure())
}
function Set-WpfDocumentationOutputPanel {
param($Form, [string]$OutputValue)
foreach($output in [DocumentationRegistry]::Outputs) {
$panel = $Form.FindName("pnlDocumentationOutput$($output.Name)")
if($panel) { $panel.Visibility = if($output.Value -eq $OutputValue) { 'Visible' } else { 'Collapsed' } }
}
}
function Show-GraphBulkDocumentationForm
{
param([object[]]$PolicyObject)
$script:bulkDocForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkDocumentationForm.xaml"), $true)
if(-not $script:bulkDocForm) { return }
$script:dgObjectsToDocument = $script:bulkDocForm.FindName("dgObjectsToDocument")
# ---- Output-format combo populated from the registry ----
$cbType = $script:bulkDocForm.FindName("cbDocumentationType")
if($cbType) {
$outputs = @([DocumentationRegistry]::Outputs |
Sort-Object Name |
ForEach-Object { [PSCustomObject]@{ Name = $_.Name; Value = $_.Value } })
$cbType.ItemsSource = $outputs
if($outputs.Count -gt 0) {
$last = Get-SettingStoreValue "Documentation" "OutputType" "json"
$match = $outputs | Where-Object Value -eq $last | Select-Object -First 1
if($match) { $cbType.SelectedValue = $match.Value }
else { $cbType.SelectedIndex = 0 }
}
Set-WpfDocumentationOutputPanel $script:bulkDocForm ([string]$cbType.SelectedValue)
$cbType.Add_SelectionChanged({
param($source, $e)
Set-WpfDocumentationOutputPanel $script:bulkDocForm ([string]$source.SelectedValue)
})
}
# ---- Restore persisted form values ----
$script:UIProvider.SetXamlProperty($script:bulkDocForm, "txtDocumentationOutputFolder", "Text", [string](Get-SettingStoreValue "Documentation" "OutputFolder" ""))
$script:UIProvider.SetXamlProperty($script:bulkDocForm, "txtDocumentFromFolder", "Text", [string](Get-DocumentationSetting 'SourceFolder' ''))
$script:UIProvider.SetXamlProperty($script:bulkDocForm, "txtDocumentFilter", "Text", [string](Get-DocumentationSetting 'NameFilter' ''))
Initialize-WpfDocumentationOptions $script:bulkDocForm
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "browseDocumentationOutputFolder", "add_click", {
$current = $script:UIProvider.GetXamlProperty($script:bulkDocForm, "txtDocumentationOutputFolder", "Text")
$folder = Get-Folder $current "Select root folder for documentation"
if($folder) {
$script:UIProvider.SetXamlProperty($script:bulkDocForm, "txtDocumentationOutputFolder", "Text", $folder)
}
})
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "browseDocumentFromFolder", "add_click", {
$current = $script:UIProvider.GetXamlProperty($script:bulkDocForm, "txtDocumentFromFolder", "Text")
$folder = Get-Folder $current "Select exported source folder"
if($folder) {
$script:UIProvider.SetXamlProperty($script:bulkDocForm, "txtDocumentFromFolder", "Text", $folder)
}
})
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "browseCSVDocumentationPath", "add_click", {
$current = $script:UIProvider.GetXamlProperty($script:bulkDocForm, "txtCSVDocumentationPath", "Text")
$folder = Get-Folder $current "Select CSV documentation folder"
if($folder) { $script:UIProvider.SetXamlProperty($script:bulkDocForm, "txtCSVDocumentationPath", "Text", $folder) }
})
# Registry-driven browse-button wiring. Each output provider that has
# file-picker inputs on the bulk-doc form attaches a FileBrowseControls
# array to its [DocumentationRegistry] entry from a matching WPF
# ClassExtensions file (UI/WPF/ClassExtensions/DocumentationOutput<X>UIExtension.ps1).
# Adding a new provider with browse buttons requires no edit here — just
# drop a ClassExtensions file that Add-Member's onto the provider.
foreach($output in [DocumentationRegistry]::Outputs) {
if(-not $output.PSObject.Properties['FileBrowseControls']) { continue }
foreach($fb in @($output.FileBrowseControls)) {
$params = @{ Form = $script:bulkDocForm; ButtonName = $fb.Button; TextName = $fb.TextBox }
if($fb.Save) { $params['Save'] = $true }
if($fb.Filter) { $params['Filter'] = $fb.Filter }
Add-WpfDocumentationFileBrowse @params
}
}
# ---- Object list (same filter as Bulk Export) ----
$script:bulkDocDocumentableGroups = @($script:IntuneGroups | Where-Object {
$_.Title -and (-not ($_.ShowButtons -is [Object[]]) -or $_.ShowButtons -contains "Document")
})
$script:bulkDocPolicyObjects = Get-UIItemsArray $PolicyObject
# Selected column with header-checkbox toggle-all
$column = Get-GridCheckboxColumn "Selected"
$script:dgObjectsToDocument.Columns.Add($column)
$column.Header.IsChecked = $true
$column.Header.add_Click({
foreach($item in $script:dgObjectsToDocument.ItemsSource) {
$item.Selected = $this.IsChecked
}
$script:dgObjectsToDocument.Items.Refresh()
})
# Title column
$binding = [System.Windows.Data.Binding]::new("Title")
$titleCol = [System.Windows.Controls.DataGridTextColumn]::new()
$titleCol.Header = if($script:bulkDocPolicyObjects.Count -gt 0) { "Title" } else { "Object type" }
$titleCol.IsReadOnly = $true
$titleCol.Binding = $binding
$script:dgObjectsToDocument.Columns.Add($titleCol)
$script:bulkDocMode = if($script:bulkDocPolicyObjects.Count -gt 0) { "Object" } else { "Group" }
$listBy = $script:bulkDocForm.FindName("pnlDocumentationListBy")
if($listBy) { $listBy.Visibility = if($script:bulkDocMode -eq "Object") { "Collapsed" } else { "Visible" } }
foreach($bulkOnlyControlName in @("lblDocumentFilter","txtDocumentFilter","lblDocumentFromFolder","pnlDocumentFromFolder")) {
$bulkOnlyControl = $script:bulkDocForm.FindName($bulkOnlyControlName)
if($bulkOnlyControl) { $bulkOnlyControl.Visibility = if($script:bulkDocMode -eq "Object") { "Collapsed" } else { "Visible" } }
}
Update-BulkDocumentationObjectList
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "rbBulkDocViewGroup", "add_Checked", {
$script:bulkDocMode = "Group"
Update-BulkDocumentationObjectList
})
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "rbBulkDocViewType", "add_Checked", {
$script:bulkDocMode = "Type"
Update-BulkDocumentationObjectList
})
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "btnClose", "add_click", {
$script:bulkDocForm = $null
$script:bulkDocPolicyObjects = $null
Show-ModalObject
})
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "btnDocument", "add_click", {
try {
Write-Status "Initializing documentation"
$selection = if($script:bulkDocMode -eq "Object") { Get-BulkDocumentationSelectedObjects } else { Get-BulkDocumentationSelectedIds }
$unit = if($script:bulkDocMode -eq "Object") { "policy" } elseif($script:bulkDocMode -eq "Type") { "policy type" } else { "object group" }
if($selection.Count -eq 0) {
$script:UIProvider.ShowMessageBox("Select at least one $unit to document.", "Bulk Documentation", "OK", "Warning") | Out-Null
return
}
$cb = $script:bulkDocForm.FindName("cbDocumentationType")
$outputValue = if($cb) { [string]$cb.SelectedValue } else { $null }
if(-not $outputValue) {
$script:UIProvider.ShowMessageBox("Select an output format.", "Bulk Documentation", "OK", "Warning") | Out-Null
return
}
$rawFolder = $script:UIProvider.GetXamlProperty($script:bulkDocForm, "txtDocumentationOutputFolder", "Text")
if($rawFolder) { $rawFolder = ([string]$rawFolder).Trim().Trim('"').Trim("'") }
$options = Get-WpfDocumentationOptions $script:bulkDocForm
if(-not $rawFolder) {
# Registry-driven fallback: consult the provider's declared
# PrimaryPathOption to find its persisted path, and PathIsFolder to
# decide whether the persisted value is already a folder (CSV) or a
# file whose parent we need to take. Adding a provider requires no
# change here — it just declares the two fields on registration.
$provider = [DocumentationRegistry]::FindOutput($outputValue)
$selectedPath = if($provider -and $provider.PSObject.Properties['PrimaryPathOption'] -and $provider.PrimaryPathOption) {
$providerOpts = $options.Outputs[$outputValue]
if($providerOpts) { $providerOpts[$provider.PrimaryPathOption] }
}
if($selectedPath) {
$rawFolder = if($provider -and $provider.PSObject.Properties['PathIsFolder'] -and $provider.PathIsFolder) { $selectedPath }
else { Split-Path -Parent $selectedPath }
}
if(-not $rawFolder) { $rawFolder = [Environment]::GetFolderPath('MyDocuments') }
}
try { $resolvedFolder = [IO.Path]::GetFullPath($rawFolder) }
catch {
$script:UIProvider.ShowMessageBox("Invalid output folder path: $($_.Exception.Message)", "Bulk Documentation", "OK", "Error") | Out-Null
return
}
$language = [string]$options.Language
if(-not $language) { $language = 'en' }
$defaultName = "%Organization%-%Date%"
if(-not $options.Outputs.csv.CSVDocumentationPath) { $options.Outputs.csv.CSVDocumentationPath = $resolvedFolder }
if(-not $options.Outputs.json.JSONDocumentName) { $options.Outputs.json.JSONDocumentName = Join-Path $resolvedFolder "$defaultName.json" }
if(-not $options.Outputs.html.HTMLDocumentName) { $options.Outputs.html.HTMLDocumentName = Join-Path $resolvedFolder "$defaultName.html" }
# Confluence storage format is XHTML, so .html keeps it openable in a browser.
if(-not $options.Outputs.atlassian.AtlassianDocumentName) { $options.Outputs.atlassian.AtlassianDocumentName = Join-Path $resolvedFolder "$defaultName.html" }
if(-not $options.Outputs.md.MDDocumentName) { $options.Outputs.md.MDDocumentName = Join-Path $resolvedFolder "$defaultName.md" }
if(-not $options.Outputs.word.WordDocumentName) { $options.Outputs.word.WordDocumentName = Join-Path $resolvedFolder "$defaultName.docx" }
# Persist form state for next time
Save-SettingStoreValue "Documentation" "OutputType" $outputValue
Save-SettingStoreValue "Documentation" "OutputFolder" $resolvedFolder
Save-DocumentationOptionsDefaults $options
$startParams = @{
OutputFormat = $outputValue
Language = $language
Options = $options
}
if($script:bulkDocMode -ne "Object" -and $options.SourceFolder) { $startParams.SourceFolder = $options.SourceFolder }
if($script:bulkDocMode -eq "Type") {
$startParams.PolicyType = $selection
}
elseif($script:bulkDocMode -eq "Group") {
$startParams.PolicyGroup = $selection
}
Write-Log "Documentation starting. Mode=$script:bulkDocMode Format=$outputValue Folder=$resolvedFolder Selection=$($selection -join ',')"
try {
if($script:bulkDocMode -eq "Object") {
[void]($selection | Start-GraphBulkDocumentation @startParams)
}
else {
[void](Start-GraphBulkDocumentation @startParams)
}
Write-Status $null
$script:UIProvider.ShowMessageBox(("Documentation finished. Output folder:`n{0}" -f $resolvedFolder), "Bulk Documentation", "OK", "Information") | Out-Null
}
catch {
Write-LogError "Bulk documentation failed" $_.Exception
$script:UIProvider.ShowMessageBox("Bulk documentation failed: $($_.Exception.Message)", "Bulk Documentation", "OK", "Error") | Out-Null
}
}
finally {
Write-Status $null
}
})
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "btnPreviewDocumentation", "add_click", {
$selection = if($script:bulkDocMode -eq "Object") { Get-BulkDocumentationSelectedObjects } else { Get-BulkDocumentationSelectedIds }
if($selection.Count -eq 0) { return }
$options = Get-WpfDocumentationOptions $script:bulkDocForm
$params = if($script:bulkDocMode -eq 'Type') { @{PolicyType=$selection} } elseif($script:bulkDocMode -eq 'Group') { @{PolicyGroup=$selection} } else { @{} }
if($script:bulkDocMode -eq 'Object') {
$policies = $selection
}
elseif($options.SourceFolder) {
$options.SourceTenantUnavailable = $true
Initialize-DocumentationSourceTenantContext -SourceFolder $options.SourceFolder
$policies = Get-DocumentationPoliciesFromSourceFolder -SourceFolder $options.SourceFolder @params
} else {
$policies = Get-GraphPolicies @params
}
$script:bulkDocPreview = @($policies | Select-Object -First 5 | Get-GraphDocumentation -Language $options.Language -Options $options)
$script:bulkDocForm.FindName('txtDocumentationRawData').Text = ($script:bulkDocPreview | ConvertTo-Json -Depth 10)
})
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "btnCopyBasicDocumentation", "add_click", {
@($script:bulkDocPreview | ForEach-Object BasicInfo) | Select-Object Name,Value | ConvertTo-Csv -NoTypeInformation | Set-Clipboard
})
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "btnCopySettingsDocumentation", "add_click", {
@($script:bulkDocPreview | ForEach-Object FilteredSettings) | ConvertTo-Csv -NoTypeInformation | Set-Clipboard
})
$script:UIProvider.AddXamlEvent($script:bulkDocForm, "btnCopyRawDocumentation", "add_click", {
$script:bulkDocForm.FindName('txtDocumentationRawData').Text | Set-Clipboard
})
$title = if($script:bulkDocMode -eq 'Object') { 'Document' } else { 'Bulk Documentation' }
$script:UIProvider.ShowModalForm($title, $script:bulkDocForm, $true)
}
function Update-BulkDocumentationObjectList
{
if(-not $script:dgObjectsToDocument) { return }
$script:documentObjects = @()
if($script:bulkDocMode -eq "Object") {
foreach($policy in ($script:bulkDocPolicyObjects | Sort-Object Name)) {
$script:documentObjects += New-Object PSObject -Property @{
Title = $policy.Name
Selected = $true
ObjectGroup = $null
ObjectType = $policy.PolicyType
PolicyObject = $policy
}
}
}
elseif($script:bulkDocMode -eq "Type") {
$documentableGroupIds = @($script:bulkDocDocumentableGroups | ForEach-Object { $_.Id })
$sortedTypes = $script:IntuneTypes |
Where-Object { $_.PolicyGroup -and ($documentableGroupIds -contains $_.PolicyGroup.Id) } |
Sort-Object Title
foreach($intuneType in $sortedTypes) {
$script:documentObjects += New-Object PSObject -Property @{
Title = $intuneType.Title
Selected = $true
ObjectGroup = $null
ObjectType = $intuneType
}
}
}
else {
$sortedGroups = $script:bulkDocDocumentableGroups | Sort-Object Title
foreach($intuneGroup in $sortedGroups) {
$script:documentObjects += New-Object PSObject -Property @{
Title = $intuneGroup.Title
Selected = $true
ObjectGroup = $intuneGroup
ObjectType = $null
}
}
}
$script:dgObjectsToDocument.ItemsSource = $script:documentObjects
}
function Get-BulkDocumentationSelectedIds
{
if($script:bulkDocMode -eq "Type") {
return @($script:documentObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectType -and $_.ObjectType.Id } |
ForEach-Object { $_.ObjectType.Id })
}
return @($script:documentObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectGroup -and $_.ObjectGroup.Id } |
ForEach-Object { $_.ObjectGroup.Id })
}
function Get-BulkDocumentationSelectedObjects
{
return @($script:documentObjects |
Where-Object { $_.Selected -eq $true -and $_.PolicyObject } |
ForEach-Object { $_.PolicyObject })
}
# Compatibility wrapper for the main-view Document button. The shared form
# switches to Object mode and displays the selected policies.
function Show-IntuneManagerDocumentForm
{
param([object[]]$PolicyObject)
if (-not $PolicyObject -or $PolicyObject.Count -eq 0) {
$script:UIProvider.ShowMessageBox("No items to document.", "Document", "OK", "Warning") | Out-Null
return
}
Show-GraphBulkDocumentationForm -PolicyObject $PolicyObject
}
#endregion
#endregion
@@ -0,0 +1,210 @@
# Bulk Export form.
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Show-GraphBulkExportForm
{
$script:bulkExportForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkExportForm.xaml"), $true)
if(-not $script:bulkExportForm) { return }
$script:dgObjectsToExport = $script:bulkExportForm.FindName("dgObjectsToExport")
$exportSettings = [IntuneManagerExportSettings]::new()
$script:bulkExportForm.DataContext = $exportSettings
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "browseExportPath", "add_click", {
$folder = Get-Folder ($script:UIProvider.GetXamlProperty($script:bulkExportForm, "txtExportPath", "Text")) "Select root folder for export"
if($folder)
{
$script:bulkExportForm.DataContext.ExportFolder = $folder
$tmp = $script:bulkExportForm.DataContext
$script:bulkExportForm.DataContext = $null
$script:bulkExportForm.DataContext = $tmp
}
})
# Register dynamic export-properties / UI extensions for every exportable
# policy type up front. Done once (not per mode switch) so toggling
# Group/API doesn't re-register and we never miss a type that only appears
# in the other view.
$script:bulkExportExportableGroups = @($script:IntuneGroups | Where-Object {
$_.Title -and (-not ($_.ShowButtons -is [Object[]]) -or $_.ShowButtons -contains "Export")
})
foreach($intuneGroup in $script:bulkExportExportableGroups)
{
$intuneGroup.PolicyTypes | Add-IntuneManagerExportProperties -ExportSettings $exportSettings
$intuneGroup.PolicyTypes | Add-IntuneManagerExportUIExtensions -Form $script:bulkExportForm
}
$column = Get-GridCheckboxColumn "Selected"
$script:dgObjectsToExport.Columns.Add($column)
$column.Header.IsChecked = $true # All items are checked by default
$column.Header.add_Click({
foreach($Item in $script:dgObjectsToExport.ItemsSource)
{
$Item.Selected = $this.IsChecked
}
$script:dgObjectsToExport.Items.Refresh()
}
)
# Add Object type column
$binding = [System.Windows.Data.Binding]::new("Title")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Object type"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:dgObjectsToExport.Columns.Add($column)
# Default view is "Group" (matches the IsChecked=True on the XAML radio).
$script:bulkExportMode = "Group"
Update-BulkExportObjectList
# Repopulate the grid when the user flips between Group and API views.
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "rbBulkExportViewGroup", "add_Checked", {
$script:bulkExportMode = "Group"
Update-BulkExportObjectList
})
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "rbBulkExportViewType", "add_Checked", {
$script:bulkExportMode = "Type"
Update-BulkExportObjectList
})
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "btnClose", "add_click", {
$script:bulkExportForm = $null
Show-ModalObject
})
# NOTE: the "Save settings for batch job" button was removed 2026-06-12 per
# user decision — scheduled/CI runs use the public drivers directly
# (Start-GraphBulkExport -SettingsFile files can still be produced with
# Save-GraphBulkExportSettings from a script).
# btnExport: collect the selected rows from the DataGrid and call the public
# driver. The UI does NOT contain export logic — it's a thin caller of
# Start-GraphBulkExport so the same operation is scriptable headlessly.
$script:UIProvider.AddXamlEvent($script:bulkExportForm, "btnExport", "add_click", {
$selection = Get-BulkExportSelectedIds
$unit = if ($script:bulkExportMode -eq "Type") { "policy type" } else { "object group" }
if ($selection.Count -eq 0) {
$script:UIProvider.ShowMessageBox("Select at least one $unit to export.", "Bulk Export", "OK", "Warning") | Out-Null
return
}
# Read the path straight from the TextBox rather than relying on the binding —
# the field is bound TwoWay/PropertyChanged so they should agree, but pulling
# from the control directly is bullet-proof. Trim and unquote (users sometimes
# paste paths with surrounding quotes).
$rawPath = ($script:UIProvider.GetXamlProperty($script:bulkExportForm, "txtExportPath", "Text"))
if ($rawPath) { $rawPath = ([string]$rawPath).Trim().Trim('"').Trim("'") }
if (-not $rawPath) {
$script:UIProvider.ShowMessageBox("Select an export folder.", "Bulk Export", "OK", "Warning") | Out-Null
return
}
# Resolve to a full path so the success message shows the canonical location,
# not whatever relative-ish text the user typed.
try { $resolvedPath = [IO.Path]::GetFullPath($rawPath) }
catch {
$script:UIProvider.ShowMessageBox("Invalid export folder path: $($_.Exception.Message)", "Bulk Export", "OK", "Error") | Out-Null
return
}
$script:bulkExportForm.DataContext.ExportFolder = $resolvedPath
Write-Log "Bulk export starting. Folder: $resolvedPath"
$startParams = @{
ExportSettings = $script:bulkExportForm.DataContext
}
if ($script:bulkExportMode -eq "Type") {
$startParams.PolicyType = $selection
}
else {
$startParams.PolicyGroup = $selection
}
try {
$summary = Start-GraphBulkExport @startParams
Write-Status $null
$msg = ("Exported {0} policies across {1} types ({2} failed) in {3:hh\:mm\:ss}.`n`nFolder:`n{4}" -f `
$summary.Policies, $summary.Types, $summary.Failed, $summary.Duration, $resolvedPath)
$unknownNote = Get-IntuneUnknownSelectorSummary $summary.UnknownSelectors
if($unknownNote) { $msg += "`n`n$unknownNote" }
$script:UIProvider.ShowMessageBox($msg, "Bulk Export", "OK", "Information") | Out-Null
}
catch {
Write-LogError "Bulk export failed" $_.Exception
$script:UIProvider.ShowMessageBox("Bulk export failed: $($_.Exception.Message)", "Bulk Export", "OK", "Error") | Out-Null
}
$script:bulkExportForm = $null
Show-ModalObject
})
$script:UIProvider.ShowModalForm("Bulk Export", $script:bulkExportForm, $true)
}
# Rebuild the bulk-export DataGrid for the current $script:bulkExportMode.
# Group mode lists policy groups (Configuration, Compliance, ...); Type mode
# lists individual policy types (APIs). Both sort by Title so the order is
# stable instead of class-load order.
function Update-BulkExportObjectList
{
if (-not $script:dgObjectsToExport) { return }
$script:exportObjects = @()
if ($script:bulkExportMode -eq "Type") {
$exportableGroupIds = @($script:bulkExportExportableGroups | ForEach-Object { $_.Id })
$sortedTypes = $script:IntuneTypes |
Where-Object { $_.PolicyGroup -and ($exportableGroupIds -contains $_.PolicyGroup.Id) } |
Sort-Object Title
foreach ($intuneType in $sortedTypes) {
$script:exportObjects += New-Object PSObject -Property @{
Title = $intuneType.Title
Selected = $true
ObjectGroup = $null
ObjectType = $intuneType
}
}
}
else {
$sortedGroups = $script:bulkExportExportableGroups | Sort-Object Title
foreach ($intuneGroup in $sortedGroups) {
$script:exportObjects += New-Object PSObject -Property @{
Title = $intuneGroup.Title
Selected = (?? $intuneGroup.BulkExport $true)
ObjectGroup = $intuneGroup
ObjectType = $null
}
}
}
$script:dgObjectsToExport.ItemsSource = $script:exportObjects
}
# Pull the Id list out of the currently-checked rows. In Group mode that's
# PolicyGroup IDs (for -PolicyGroup); in Type mode it's PolicyType IDs (for
# -PolicyType). Returns @() when nothing is selected.
function Get-BulkExportSelectedIds
{
if ($script:bulkExportMode -eq "Type") {
return @($script:exportObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectType -and $_.ObjectType.Id } |
ForEach-Object { $_.ObjectType.Id })
}
return @($script:exportObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectGroup -and $_.ObjectGroup.Id } |
ForEach-Object { $_.ObjectGroup.Id })
}
# ─── Bulk Scope Tags ──────────────────────────────────────────────────────────
# Thin UI wrapper around Set-GraphBulkScopeTags. The form mirrors Bulk Export's
# Group / API selector for picking object types, adds an action selector
# (Add / Replace / Remove), an orphan-cleanup checkbox, and a dual-list scope
# tag picker. All business logic lives in the public command — the UI only
# collects inputs, calls it, and shows the summary.
# Get-BulkScopeTagCatalog moved to Internal/IntuneScopeTags.ps1 so the
# Avalonia tree can share it.
@@ -0,0 +1,145 @@
# Bulk Import form.
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Show-GraphBulkImportForm
{
$script:bulkImportForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkImportForm.xaml"), $true)
if(-not $script:bulkImportForm) { return }
$script:dgObjectsToImport = $script:bulkImportForm.FindName("dgObjectsToImport")
$importSettings = [IntuneManagerImportSettings]::new()
$script:bulkImportForm.DataContext = $importSettings
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "txtImportPath", "Text", $importSettings.ImportFolder)
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "chkImportAssignments", "IsChecked", $importSettings.ImportAssignments)
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "chkImportScopes", "IsChecked", $importSettings.ImportScopeTags)
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "chkReplaceDependencyIDs", "IsChecked", $importSettings.ReplaceDependencyIDs)
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "cbImportType", "ItemsSource", $script:IntuneManagerImportOptions)
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "cbImportType", "SelectedValue", $importSettings.ImportType)
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "lblImportType", "Visibility", "Visible")
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "cbImportType", "Visibility", "Visible")
$script:importObjects = @()
foreach($intuneGroup in $script:IntuneGroups)
{
if(-not $intuneGroup.Title) { continue }
if($intuneGroup.ShowButtons -is [Object[]] -and $intuneGroup.ShowButtons -notcontains "Import") { continue }
$script:importObjects += New-Object PSObject -Property @{
Title = $intuneGroup.Title
Selected = (?? $intuneGroup.BulkImport $true)
ObjectGroup = $intuneGroup
}
$intuneGroup.PolicyTypes | Add-IntuneManagerImportUIExtensions -Form $script:bulkImportForm
}
$column = Get-GridCheckboxColumn "Selected"
$script:dgObjectsToImport.Columns.Add($column)
$column.Header.IsChecked = $true
$column.Header.add_Click({
foreach($Item in $script:dgObjectsToImport.ItemsSource)
{
$Item.Selected = $this.IsChecked
}
$script:dgObjectsToImport.Items.Refresh()
})
$binding = [System.Windows.Data.Binding]::new("Title")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Object type"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:dgObjectsToImport.Columns.Add($column)
$script:dgObjectsToImport.ItemsSource = $script:importObjects
if($importSettings.ImportFolder)
{
$migrationInfo, $sameTenant = Get-MigrationTableInfo $importSettings.ImportFolder $script:OrganizationId
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "chkReplaceDependencyIDs", "IsEnabled", ($sameTenant -eq $false))
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "chkReplaceDependencyIDs", "IsChecked", ($sameTenant -eq $false))
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "lblMigrationTableInfo", "Content", $migrationInfo)
}
$script:UIProvider.AddXamlEvent($script:bulkImportForm, "browseImportPath", "add_click", {
$folder = Get-Folder ($script:UIProvider.GetXamlProperty($script:bulkImportForm, "txtImportPath", "Text")) "Select root folder for import"
if($folder)
{
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "txtImportPath", "Text", $folder)
$script:bulkImportForm.DataContext.ImportFolder = $folder
$migrationInfo, $sameTenant = Get-MigrationTableInfo $folder $script:OrganizationId
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "chkReplaceDependencyIDs", "IsEnabled", ($sameTenant -eq $false))
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "chkReplaceDependencyIDs", "IsChecked", ($sameTenant -eq $false))
$script:UIProvider.SetXamlProperty($script:bulkImportForm, "lblMigrationTableInfo", "Content", $migrationInfo)
}
})
$script:UIProvider.AddXamlEvent($script:bulkImportForm, "btnClose", "add_click", {
$script:bulkImportForm = $null
Show-ModalObject
})
$script:UIProvider.AddXamlEvent($script:bulkImportForm, "btnImport", "add_click", {
$importFolder = Expand-FileName ($script:UIProvider.GetXamlProperty($script:bulkImportForm, "txtImportPath", "Text"))
if(-not [IO.Directory]::Exists($importFolder))
{
$script:UIProvider.ShowMessageBox("Import folder not found:`n$importFolder", "Error", "OK", "Error")
return
}
$selectedGroups = @($script:importObjects | Where-Object Selected -eq $true)
if($selectedGroups.Count -eq 0)
{
$script:UIProvider.ShowMessageBox("No object types selected.", "Error", "OK", "Error")
return
}
Save-SettingStoreValue "" "LastUsedRoot" $importFolder
$importType = $script:UIProvider.GetXamlProperty($script:bulkImportForm, "cbImportType", "SelectedValue")
$nameFilter = ($script:UIProvider.GetXamlProperty($script:bulkImportForm, "txtImportNameFilter", "Text")).Trim()
# The UI is a thin caller of the public driver (R10) — the same import
# runs headless via Start-GraphBulkImport, which also persists the
# checkbox settings (the import pipeline reads them via Get-SettingValue)
# and honours the ClearCacheBeforeExportImport setting.
Write-Status "Bulk import" -Block
try
{
$summary = Start-GraphBulkImport -ImportFolder $importFolder -Filter $nameFilter `
-PolicyGroup @($selectedGroups | ForEach-Object { $_.ObjectGroup.ID }) `
-ImportAssignments ([bool]($script:UIProvider.GetXamlProperty($script:bulkImportForm, "chkImportAssignments", "IsChecked"))) `
-ImportScopeTags ([bool]($script:UIProvider.GetXamlProperty($script:bulkImportForm, "chkImportScopes", "IsChecked"))) `
-ReplaceDependencyIDs ([bool]($script:UIProvider.GetXamlProperty($script:bulkImportForm, "chkReplaceDependencyIDs", "IsChecked"))) `
-ImportType $importType
$unknownNote = Get-IntuneUnknownSelectorSummary $summary.UnknownSelectors
if($summary.Imported -eq 0)
{
$msg = "No objects were imported.`nVerify the import folder and exported files."
if($unknownNote) { $msg += "`n`n$unknownNote" }
$script:UIProvider.ShowMessageBox($msg, "Import", "OK", "Warning")
}
else
{
# Success shows no dialog, so a group id that matched nothing gets its own.
if($unknownNote) { $script:UIProvider.ShowMessageBox($unknownNote, "Import", "OK", "Warning") | Out-Null }
Invoke-IntuneActivateObject $script:IntuneManagerSelectedObject | Out-Null
}
}
catch
{
Write-LogError "Bulk import failed" $_.Exception
$script:UIProvider.ShowMessageBox("Bulk import failed:`n`n$($_.Exception.Message)", "Import", "OK", "Error") | Out-Null
}
Write-Status ""
})
$script:UIProvider.ShowModalForm("Bulk Import", $script:bulkImportForm, $true)
}
@@ -0,0 +1,278 @@
# Bulk Scope Tag form.
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Show-GraphBulkScopeTagForm
{
$script:bulkScopeTagForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkScopeTagForm.xaml"))
if(-not $script:bulkScopeTagForm) { return }
$script:dgBulkScopeTagObjects = $script:bulkScopeTagForm.FindName("dgBulkScopeTagObjects")
$script:lstBulkScopeTagAvail = $script:bulkScopeTagForm.FindName("lstBulkScopeTagAvailable")
$script:lstBulkScopeTagSelected = $script:bulkScopeTagForm.FindName("lstBulkScopeTagSelected")
$script:txtBulkScopeTagStatus = $script:bulkScopeTagForm.FindName("txtBulkScopeTagStatus")
$scopeTagSettings = [IntuneManagerScopeTagSettings]::new()
$script:bulkScopeTagForm.DataContext = $scopeTagSettings
# ── Object list (mirrors Bulk Export DataGrid setup) ──────────────────────
$script:bulkScopeTagEligibleTypes = @($script:IntuneTypes | Where-Object { $_.ScopeTagProperty })
$script:bulkScopeTagEligibleGroups = @($script:IntuneGroups | Where-Object {
$_.Title -and ($_.PolicyTypes | Where-Object { $_.ScopeTagProperty })
})
$column = Get-GridCheckboxColumn "Selected"
$script:dgBulkScopeTagObjects.Columns.Add($column)
$column.Header.IsChecked = $true
$column.Header.add_Click({
foreach($item in $script:dgBulkScopeTagObjects.ItemsSource) {
$item.Selected = $this.IsChecked
}
$script:dgBulkScopeTagObjects.Items.Refresh()
})
$col = [System.Windows.Controls.DataGridTextColumn]::new()
$col.Header = "Object type"
$col.IsReadOnly = $true
$col.Binding = [System.Windows.Data.Binding]::new("Title")
$script:dgBulkScopeTagObjects.Columns.Add($col)
$script:bulkScopeTagMode = "Group"
Update-BulkScopeTagObjectList
$script:UIProvider.AddXamlEvent($script:bulkScopeTagForm, "rbBulkScopeTagViewGroup", "add_Checked", {
$script:bulkScopeTagMode = "Group"; Update-BulkScopeTagObjectList
})
$script:UIProvider.AddXamlEvent($script:bulkScopeTagForm, "rbBulkScopeTagViewType", "add_Checked", {
$script:bulkScopeTagMode = "Type"; Update-BulkScopeTagObjectList
})
# ── Action radio → settings.Action sync ───────────────────────────────────
# XAML radio buttons aren't directly bound to a string property; wire change
# handlers so the [IntuneManagerScopeTagSettings] always reflects the UI.
$script:UIProvider.AddXamlEvent($script:bulkScopeTagForm, "rbBulkScopeTagAdd", "add_Checked", { $script:bulkScopeTagForm.DataContext.Action = "Add" })
$script:UIProvider.AddXamlEvent($script:bulkScopeTagForm, "rbBulkScopeTagReplace", "add_Checked", { $script:bulkScopeTagForm.DataContext.Action = "Replace" })
$script:UIProvider.AddXamlEvent($script:bulkScopeTagForm, "rbBulkScopeTagRemove", "add_Checked", { $script:bulkScopeTagForm.DataContext.Action = "Remove" })
# ── Scope tag picker (dual-list with click-to-move) ───────────────────────
# Keep the modal open path cheap. The tenant dependency cache is used first;
# if it is cold, the live Graph fetch runs after the dialog has rendered.
$script:_bulkScopeTagAll = @()
$script:_bulkScopeTagSelectedIds = [System.Collections.Generic.HashSet[string]]::new()
$script:colBulkScopeTagAvail = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
$script:colBulkScopeTagSelected = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
$script:_syncBulkScopeTagLists = {
$script:colBulkScopeTagAvail.Clear()
$script:colBulkScopeTagSelected.Clear()
foreach($t in $script:_bulkScopeTagAll) {
if($script:_bulkScopeTagSelectedIds.Contains($t.Id)) {
[void]$script:colBulkScopeTagSelected.Add($t)
} else {
[void]$script:colBulkScopeTagAvail.Add($t)
}
}
}
& $script:_syncBulkScopeTagLists
$script:lstBulkScopeTagAvail.ItemsSource = $script:colBulkScopeTagAvail
$script:lstBulkScopeTagSelected.ItemsSource = $script:colBulkScopeTagSelected
$script:txtBulkScopeTagStatus.Text = "Loading scope tags..."
# Double-click toggles list membership in either direction; the Add/Remove
# buttons do the same for the highlighted rows (double-click alone was not
# discoverable).
$script:_bulkScopeTagAddSelected = {
$ids = @($script:lstBulkScopeTagAvail.SelectedItems | ForEach-Object { [string]$_.Id })
foreach($id in $ids) { [void]$script:_bulkScopeTagSelectedIds.Add($id) }
& $script:_syncBulkScopeTagLists
}
$script:_bulkScopeTagRemoveSelected = {
$ids = @($script:lstBulkScopeTagSelected.SelectedItems | ForEach-Object { [string]$_.Id })
foreach($id in $ids) { [void]$script:_bulkScopeTagSelectedIds.Remove($id) }
& $script:_syncBulkScopeTagLists
}
$script:lstBulkScopeTagAvail.Add_MouseDoubleClick({ & $script:_bulkScopeTagAddSelected })
$script:lstBulkScopeTagSelected.Add_MouseDoubleClick({ & $script:_bulkScopeTagRemoveSelected })
$script:UIProvider.AddXamlEvent($script:bulkScopeTagForm, "btnBulkScopeTagAddTag", "add_click", { & $script:_bulkScopeTagAddSelected })
$script:UIProvider.AddXamlEvent($script:bulkScopeTagForm, "btnBulkScopeTagRemoveTag", "add_click", { & $script:_bulkScopeTagRemoveSelected })
# ── Apply ─────────────────────────────────────────────────────────────────
$script:UIProvider.AddXamlEvent($script:bulkScopeTagForm, "btnBulkScopeTagApply", "add_click", {
# Concurrent-click guard. Apply triggers a multi-step Graph run; without
# this, a double-click (or impatient second press) fires two parallel
# bulk PATCH passes against the same selection.
$btnApply = $script:bulkScopeTagForm.FindName("btnBulkScopeTagApply")
$btnClose = $script:bulkScopeTagForm.FindName("btnBulkScopeTagClose")
$selectionIds = Get-BulkScopeTagSelectedObjectIds
$unit = if($script:bulkScopeTagMode -eq "Type") { "policy type" } else { "object group" }
if($selectionIds.Count -eq 0) {
$script:UIProvider.ShowMessageBox("Select at least one $unit to update.", "Bulk Scope Tags", "OK", "Warning") | Out-Null
return
}
$settings = $script:bulkScopeTagForm.DataContext
$settings.ScopeTagIds = @($script:_bulkScopeTagSelectedIds)
# Preflight guards
if($settings.ScopeTagIds.Count -eq 0 -and -not $settings.CleanupOrphans) {
$script:UIProvider.ShowMessageBox("Pick at least one scope tag, or enable 'Cleanup orphans' for a pure cleanup pass.", "Bulk Scope Tags", "OK", "Warning") | Out-Null
return
}
# Mass-wipe guard: Replace with no selected tags will strip every tag
# (including Default) from every matched policy. Defending here rather
# than in the public command keeps the cmdlet itself non-prompting.
if($settings.Action -eq "Replace" -and $settings.ScopeTagIds.Count -eq 0) {
$proceed = $script:UIProvider.ShowMessageBox(
"Replace with no scope tags selected will REMOVE every tag (including Default) from every policy that matches the filter.`n`nMost Intune policies require the Default tag and the API will reject empty roleScopeTagIds - you may end up with a large failure count.`n`nContinue?",
"Confirm wipe-all", "YesNo", "Warning")
if($proceed -ne "Yes") { return }
}
# Default-missing soft guard — mirrors the Details-view "Default scope
# tag missing" warning. Triggers for Add (tags selected but Default
# not included) and Replace (tags selected but Default not included).
# Remove is unaffected because we only remove what the user picked.
if($settings.Action -in @("Add","Replace") -and
$settings.ScopeTagIds.Count -gt 0 -and
$settings.ScopeTagIds -notcontains "0") {
$proceed = $script:UIProvider.ShowMessageBox(
"The 'Default' scope tag (Id 0) is not in the selected list.`n`nIntune usually requires Default to be present; PATCH calls may fail for policies that end up without it.`n`nContinue anyway?",
"Default scope tag missing", "YesNo", "Warning")
if($proceed -ne "Yes") { return }
}
$tagNames = @($script:_bulkScopeTagAll |
Where-Object { $script:_bulkScopeTagSelectedIds.Contains($_.Id) } |
ForEach-Object { $_.Name })
$tagList = if($tagNames.Count -gt 0) { $tagNames -join ", " } else { "(none - cleanup only)" }
$cleanup = if($settings.CleanupOrphans) { "yes" } else { "no" }
$filterText = if($settings.Filter) { $settings.Filter } else { "(none)" }
$confirm = $script:UIProvider.ShowMessageBox(@"
About to update scope tags on every policy that matches:
Action : $($settings.Action)
Tags : $tagList
Cleanup orphans : $cleanup
Name filter : $filterText
$unit count : $($selectionIds.Count)
Continue?
"@, "Confirm Bulk Scope Tag update", "YesNo", "Warning")
if($confirm -ne "Yes") { return }
# Disable both buttons for the duration of the run so a stray click
# doesn't restart it (Apply) or yank the form mid-flight (Close).
if($btnApply) { $btnApply.IsEnabled = $false }
if($btnClose) { $btnClose.IsEnabled = $false }
# No pre-call Write-Status: Set-GraphBulkScopeTags now sets its own two-line
# status ("Bulk scope tags — <Action>" + sub-step) as soon as it starts.
$startParams = @{ ScopeTagSettings = $settings }
if($script:bulkScopeTagMode -eq "Type") { $startParams.PolicyType = $selectionIds }
else { $startParams.PolicyGroup = $selectionIds }
try {
$summary = Set-GraphBulkScopeTags @startParams
Write-Status $null
$msg = ("Scanned: {0}`nMatched filter: {1}`nUpdated: {2}`nNo change: {3}`nFailed: {4}`nDuration: {5:hh\:mm\:ss}" -f `
$summary.PoliciesScanned, $summary.PoliciesMatched, $summary.PoliciesUpdated, $summary.PoliciesSkipped, $summary.PoliciesFailed, $summary.Duration)
$script:txtBulkScopeTagStatus.Text = ("Last run: updated {0}, no change {1}, failed {2}" -f $summary.PoliciesUpdated, $summary.PoliciesSkipped, $summary.PoliciesFailed)
$unknownNote = Get-IntuneUnknownSelectorSummary $summary.UnknownSelectors
if($unknownNote) { $msg += "`n`n$unknownNote" }
$script:UIProvider.ShowMessageBox($msg, "Bulk Scope Tags", "OK", "Information") | Out-Null
# Reflect the new tag values in the main DataGrid (Set-GraphBulkScopeTags
# already mirrored Object.<prop> + cleared _ScopeTags on updated rows).
try { $script:dgIntuneManagerObjects.Items.Refresh() }
catch { Write-LogDebug "Main grid refresh failed after bulk scope tag run: $($_.Exception.Message)" }
}
catch {
Write-LogError "Bulk Scope Tags run failed" $_.Exception
$script:UIProvider.ShowMessageBox("Bulk Scope Tags run failed: $($_.Exception.Message)", "Bulk Scope Tags", "OK", "Error") | Out-Null
}
finally {
if($btnApply) { $btnApply.IsEnabled = $true }
if($btnClose) { $btnClose.IsEnabled = $true }
}
})
$script:UIProvider.AddXamlEvent($script:bulkScopeTagForm, "btnBulkScopeTagClose", "add_click", {
$script:bulkScopeTagForm = $null
Show-ModalObject
})
$script:UIProvider.ShowModalForm("Bulk Scope Tags", $script:bulkScopeTagForm, $true)
try {
$scopeTagLoadError = $null
$script:_bulkScopeTagAll = @(Get-BulkScopeTagCatalog -TokenId (Get-DefaultTokenId) -LoadError ([ref]$scopeTagLoadError))
& $script:_syncBulkScopeTagLists
$script:txtBulkScopeTagStatus.Text = "$($script:_bulkScopeTagAll.Count) scope tag(s) loaded"
if($scopeTagLoadError) {
$script:UIProvider.ShowMessageBox("Could not load scope tags from the tenant.`n`n$scopeTagLoadError`n`nThe picker will only show 'Default'.", "Bulk Scope Tags", "OK", "Warning") | Out-Null
}
}
catch {
Write-LogError "Bulk Scope Tags: failed to initialize scope tag picker" $_.Exception
$script:txtBulkScopeTagStatus.Text = "Scope tag loading failed"
}
}
# Rebuild the bulk-scope-tag object DataGrid for the current view mode.
function Update-BulkScopeTagObjectList
{
if(-not $script:dgBulkScopeTagObjects) { return }
$script:bulkScopeTagObjects = @()
if($script:bulkScopeTagMode -eq "Type") {
$sortedTypes = $script:bulkScopeTagEligibleTypes | Sort-Object Title
foreach($pt in $sortedTypes) {
$script:bulkScopeTagObjects += New-Object PSObject -Property @{
Title = $pt.Title
Selected = $true
ObjectGroup = $null
ObjectType = $pt
}
}
}
else {
$sortedGroups = $script:bulkScopeTagEligibleGroups | Sort-Object Title
foreach($grp in $sortedGroups) {
$script:bulkScopeTagObjects += New-Object PSObject -Property @{
Title = $grp.Title
Selected = $true
ObjectGroup = $grp
ObjectType = $null
}
}
}
$script:dgBulkScopeTagObjects.ItemsSource = $script:bulkScopeTagObjects
}
function Get-BulkScopeTagSelectedObjectIds
{
if($script:bulkScopeTagMode -eq "Type") {
return @($script:bulkScopeTagObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectType -and $_.ObjectType.Id } |
ForEach-Object { $_.ObjectType.Id })
}
return @($script:bulkScopeTagObjects |
Where-Object { $_.Selected -eq $true -and $_.ObjectGroup -and $_.ObjectGroup.Id } |
ForEach-Object { $_.ObjectGroup.Id })
}
# ─── Bulk Assignments ─────────────────────────────────────────────────────────
# Thin UI wrapper around Set-GraphBulkAssignments. Mirrors the Bulk Scope Tag
# form's structure: Action radio, name filter, eligible-type selector
# (Group/API), plus an assignments list with Add buttons. Phase 1 supports
# group / exclusion-group / all-devices / all-users targets with optional
# assignment filters — app intent + per-platform settings come in later phases.
@@ -0,0 +1,861 @@
Add-AppEventHandler "CompareColumnVisibilityChanged" "Set-CompareGridColumnVisibility"
function Set-CompareGridColumnVisibility
{
param($ShowCategory = $false, $ShowSubCategory = $false)
if(-not $script:dgCompareInfo) { return }
foreach($col in $script:dgCompareInfo.Columns)
{
$Path = $col.Binding.Path.Path
if($Path -eq "Category") { $col.Visibility = if($ShowCategory) { "Visible" } else { "Collapsed" } }
if($Path -eq "SubCategory") { $col.Visibility = if($ShowSubCategory) { "Visible" } else { "Collapsed" } }
}
}
function Set-CompareRuntimeOptionsFromUI
{
$params = @{}
if($script:cbCompareType)
{
$params.CompareType = $script:cbCompareType.SelectedValue
$params.CompareDefinition = $script:cbCompareType.SelectedItem
}
if($script:chkIgnoreCoreProperties)
{
$params.IgnoreCoreProperties = [bool]$script:chkIgnoreCoreProperties.IsChecked
}
if($script:cbCompareSave)
{
$params.SaveType = $script:cbCompareSave.SelectedValue
}
if($script:cbCompareOutputFormat)
{
$params.OutputProvider = $script:cbCompareOutputFormat.SelectedItem
}
if($script:cbCompareCSVDelimiter)
{
$params.CsvDelimiter = $script:cbCompareCSVDelimiter.Text
}
if($script:cbCompareMultiValueDelimiter -and $null -ne $script:cbCompareMultiValueDelimiter.SelectedValue)
{
$params.ObjectSeparator = [string]$script:cbCompareMultiValueDelimiter.SelectedValue
}
if($script:chkSkipCompareAssignments)
{
$params.SkipAssignments = [bool]$script:chkSkipCompareAssignments.IsChecked
}
Set-CompareRuntimeOptions @params
}
function Get-CompareMultiValueDelimiterItems
{
@(
[PSCustomObject]@{ Name = "New line"; Value = [System.Environment]::NewLine },
[PSCustomObject]@{ Name = ";"; Value = ";" },
[PSCustomObject]@{ Name = "|"; Value = "|" }
)
}
function Initialize-CompareSharedOptionControls
{
# Controls shared by the single and bulk compare forms.
if($script:cbCompareMultiValueDelimiter)
{
$script:cbCompareMultiValueDelimiter.ItemsSource = @(Get-CompareMultiValueDelimiterItems)
$script:cbCompareMultiValueDelimiter.SelectedValue = (Get-SettingStoreValue "Compare" "ObjectSeparator" ([System.Environment]::NewLine))
if($null -eq $script:cbCompareMultiValueDelimiter.SelectedItem)
{
$script:cbCompareMultiValueDelimiter.SelectedIndex = 0
}
}
if($script:chkSkipCompareAssignments)
{
$script:chkSkipCompareAssignments.IsChecked = ((Get-SettingStoreValue "Compare" "SkipCompareAssignments" "false") -eq "true")
}
}
function Save-CompareSharedOptionSettings
{
if($script:cbCompareMultiValueDelimiter -and $null -ne $script:cbCompareMultiValueDelimiter.SelectedValue)
{
Save-SettingStoreValue "Compare" "ObjectSeparator" $script:cbCompareMultiValueDelimiter.SelectedValue
}
if($script:chkSkipCompareAssignments)
{
Save-SettingStoreValue "Compare" "SkipCompareAssignments" $(if($script:chkSkipCompareAssignments.IsChecked) { "true" } else { "false" })
}
}
# ─── Object Picker ────────────────────────────────────────────────────────────
function Show-ObjectPickerDialog
{
param(
[string] $Title,
[object[]] $Items,
[object[]] $DisplayColumns,
[scriptblock] $LoadHandler,
[string] $LoadLabel = "Load all from Intune",
# Entries from Get-PolicySearchScopes. When supplied, the dialog shows
# a "Search in" dropdown above the search box.
$Scopes = $null,
[string] $SelectedScopeKey = $null,
# Entries from Get-PolicySearchTenants. When supplied, the dialog shows
# a "Tenant" dropdown so the user can search a different tenant.
$Tenants = $null,
[string] $SelectedTenantKey = $null,
# Called as & $SearchHandler $searchText $scope and expected to return
# the rows to display. When absent the Search button filters the
# supplied -Items client-side (the original behaviour).
[scriptblock] $SearchHandler = $null
)
# Per-call state on $script:_pickerState. Closures below dynamically read
# this slot at INVOCATION time instead of capturing locals via
# .GetNewClosure() — that captures every variable visible at create time,
# including module-scope ones like $script:dgPickerObjects which are
# populated AFTER the closures are built (Show-PickerDialog assigns them
# when it materialises the dialog). Captured-as-null is what produced the
# "You cannot call a method on a null-valued expression" errors at
# $script:dgPickerObjects.Columns.Add and the 'cannot be found' error at
# $script:dgPickerObjects.ItemsSource. Using a $script: bag keeps lookup
# late-bound; assignments by Show-PickerDialog land in the same scope the
# closures read from.
$script:_pickerState = @{
Columns = $DisplayColumns
# ItemsBox lets the LoadHandler replace the list in-place; the apply
# path reassigns ItemsBox.Items and every closure picks up the new
# array on the next read.
ItemsBox = @{ Items = Get-UIItemsArray $Items }
Result = @{ Value = $null }
SearchHandler = $SearchHandler
}
$initHandler = {
$ps = $script:_pickerState
foreach($colDef in $ps.Columns)
{
$col = [System.Windows.Controls.DataGridTextColumn]::new()
$col.Header = $colDef.Header
$col.IsReadOnly = $true
$col.Binding = [System.Windows.Data.Binding]::new($colDef.Binding)
$script:dgPickerObjects.Columns.Add($col)
}
$script:dgPickerObjects.ItemsSource = $ps.ItemsBox.Items
}
$clickHandler = {
$ps = $script:_pickerState
$filter = $script:txtPickerSearch.Text
if($ps.SearchHandler)
{
# Server-side search: ask the caller for rows matching the text in
# the selected scope, and show exactly what comes back.
$scope = Get-PickerSelectedScope
try {
$script:pickerDialog.Cursor = [System.Windows.Input.Cursors]::Wait
$found = Get-UIItemsArray (& $ps.SearchHandler $filter $scope)
}
catch {
Write-LogError "Picker SearchHandler failed" $_.Exception
$found = @()
}
finally {
$script:pickerDialog.Cursor = $null
}
# The status line belongs to the search handler - it knows whether
# an empty result means "no matches" or "keep typing".
$ps.ItemsBox.Items = $found
$script:dgPickerObjects.ItemsSource = $found
return
}
if([string]::IsNullOrEmpty($filter))
{
$script:dgPickerObjects.ItemsSource = $ps.ItemsBox.Items
}
else
{
$script:dgPickerObjects.ItemsSource = @($ps.ItemsBox.Items | Where-Object { $_.Name -ilike "*$filter*" })
}
}
$okHandler = {
$script:_pickerState.Result.Value = $script:dgPickerObjects.SelectedItem
$script:pickerDialog.Close()
}
$wrappedLoadHandler = $null
if($LoadHandler) {
# Stash the user-supplied loader so the wrapper script block can
# reach it via $script: at runtime (same late-bind reasoning as
# above — no closure capture).
$script:_pickerState.UserLoadHandler = $LoadHandler
$script:_pickerState.ClickHandler = $clickHandler
$wrappedLoadHandler = {
$ps = $script:_pickerState
$newItems = & $ps.UserLoadHandler
if($ps.SearchHandler) {
# Scoped loads replace rather than merge - the returned set IS
# everything in the newly selected scope, and merging would
# leave stale rows from a previous scope in the grid.
$ps.ItemsBox.Items = Get-UIItemsArray $newItems
$script:dgPickerObjects.ItemsSource = $ps.ItemsBox.Items
return
}
if($newItems) {
# Dedupe by Id; new entries appended so cached items stay first.
$existingIds = @{}
foreach($existing in $ps.ItemsBox.Items) {
if($existing -and $existing.Id) { $existingIds[$existing.Id] = $true }
}
$merged = [System.Collections.Generic.List[object]]::new()
foreach($existing in $ps.ItemsBox.Items) { [void]$merged.Add($existing) }
foreach($n in $newItems) {
if(-not $n) { continue }
if($n.Id -and $existingIds.ContainsKey($n.Id)) { continue }
[void]$merged.Add($n)
}
$ps.ItemsBox.Items = $merged.ToArray()
}
& $ps.ClickHandler
}
}
try {
Show-PickerDialog -title $Title -initHandler $initHandler -clickHandler $clickHandler -okHandler $okHandler -LoadHandler $wrappedLoadHandler -LoadLabel $LoadLabel -Scopes $Scopes -SelectedScopeKey $SelectedScopeKey -Tenants $Tenants -SelectedTenantKey $SelectedTenantKey
return $script:_pickerState.Result.Value
}
finally {
# Avoid keeping the closed-over Items / handlers alive between dialog
# invocations; next Show-ObjectPickerDialog call replaces this anyway.
$script:_pickerState = $null
}
}
# ─── Bulk Compare ─────────────────────────────────────────────────────────────
function Show-GraphBulkCompareForm
{
$script:bulkCompareForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\BulkCompare.xaml"), $true)
if(-not $script:bulkCompareForm) { return }
$script:cbCompareProvider.ItemsSource = @($script:compareProviders)
$script:cbCompareProvider.SelectedValue = (Get-SettingStoreValue "Compare" "Provider" "export")
# A saved provider id that no longer registers (a provider was removed) must
# not leave the combo blank - Start would then be a silent no-op. Mirrors the
# Avalonia form's first-item fallback.
if($null -eq $script:cbCompareProvider.SelectedItem -and $script:cbCompareProvider.Items.Count -gt 0)
{
$script:cbCompareProvider.SelectedIndex = 0
}
$script:cbCompareSave.ItemsSource = @($script:compareOutputTypes)
$script:cbCompareSave.SelectedValue = (Get-SettingStoreValue "Compare" "SaveType" "objectType")
$script:cbCompareOutputFormat.ItemsSource = @($script:compareOutputProviders)
$script:cbCompareOutputFormat.SelectedValue = (Get-SettingStoreValue "Compare" "OutputFormat" "csv")
$script:cbCompareType.ItemsSource = $script:comparisonTypes
$script:cbCompareType.SelectedValue = (Get-SettingStoreValue "Compare" "Type" "property")
$script:cbCompareCSVDelimiter.ItemsSource = @("", ",", ";", "-", "|")
$script:cbCompareCSVDelimiter.SelectedValue = (Get-SettingStoreValue "Compare" "Delimiter" ";")
Initialize-CompareSharedOptionControls
if($script:chkIgnoreCoreProperties)
{
$script:chkIgnoreCoreProperties.IsChecked = ((Get-SettingStoreValue "Compare" "IgnoreCoreProperties" "true") -eq "true")
}
if($script:chkSkipMissingSourcePolicies)
{
$script:chkSkipMissingSourcePolicies.IsChecked = ((Get-SettingStoreValue "Compare" "SkipMissingSourcePolicies" "false") -eq "true")
}
if($script:chkSkipMissingDestinationPolicies)
{
$script:chkSkipMissingDestinationPolicies.IsChecked = ((Get-SettingStoreValue "Compare" "SkipMissingDestinationPolicies" "false") -eq "true")
}
$script:compareObjects = @()
foreach($intuneGroup in $script:IntuneGroups)
{
if(-not $intuneGroup.Title) { continue }
$script:compareObjects += [PSCustomObject]@{
Title = $intuneGroup.Title
Selected = $true
ObjectGroup = $intuneGroup
}
}
$column = Get-GridCheckboxColumn "Selected"
$script:dgObjectsToCompare.Columns.Add($column)
$column.Header.IsChecked = $true
$column.Header.Add_Click({
foreach($item in $script:dgObjectsToCompare.ItemsSource) { $item.Selected = $this.IsChecked }
$script:dgObjectsToCompare.Items.Refresh()
})
$col = [System.Windows.Controls.DataGridTextColumn]::new()
$col.Header = "Object type"
$col.IsReadOnly = $true
$col.Binding = [System.Windows.Data.Binding]::new("Title")
$script:dgObjectsToCompare.Columns.Add($col)
$script:dgObjectsToCompare.ItemsSource = $script:compareObjects
$script:cbCompareOutputFormat.Add_SelectionChanged({
$isCSV = ($script:cbCompareOutputFormat.SelectedItem -is [CompareCSVOutputProvider])
$script:cbCompareCSVDelimiter.IsEnabled = $isCSV
})
$script:UIProvider.AddXamlEvent($script:bulkCompareForm, "btnClose", "add_click", {
$script:bulkCompareForm = $null
$script:UIProvider.ShowModalObject()
})
$script:UIProvider.AddXamlEvent($script:bulkCompareForm, "btnStartCompare", "add_click", {
Write-Status "Compare objects"
Save-SettingStoreValue "Compare" "Provider" $script:cbCompareProvider.SelectedValue
Save-SettingStoreValue "Compare" "Type" $script:cbCompareType.SelectedValue
Save-SettingStoreValue "Compare" "Delimiter" $script:cbCompareCSVDelimiter.SelectedValue
Save-SettingStoreValue "Compare" "OutputFormat" $script:cbCompareOutputFormat.SelectedValue
Save-SettingStoreValue "Compare" "SaveType" $script:cbCompareSave.SelectedValue
Save-CompareSharedOptionSettings
if($script:chkIgnoreCoreProperties)
{
Save-SettingStoreValue "Compare" "IgnoreCoreProperties" $(if($script:chkIgnoreCoreProperties.IsChecked) { "true" } else { "false" })
}
try
{
Set-CompareRuntimeOptionsFromUI
$Provider = $script:cbCompareProvider.SelectedItem
if($Provider)
{
if($script:chkSkipMissingSourcePolicies)
{
$Provider.SkipMissingSourcePolicies = [bool]$script:chkSkipMissingSourcePolicies.IsChecked
Save-SettingStoreValue "Compare" "SkipMissingSourcePolicies" $(if($Provider.SkipMissingSourcePolicies) { "true" } else { "false" })
}
if($script:chkSkipMissingDestinationPolicies)
{
$Provider.SkipMissingDestinationPolicies = [bool]$script:chkSkipMissingDestinationPolicies.IsChecked
Save-SettingStoreValue "Compare" "SkipMissingDestinationPolicies" $(if($Provider.SkipMissingDestinationPolicies) { "true" } else { "false" })
}
}
$selectedGroups = @()
if($script:dgObjectsToCompare) {
$selectedGroups = @($script:dgObjectsToCompare.ItemsSource | Where-Object Selected -eq $true | ForEach-Object { $_.ObjectGroup })
}
if($Provider) { Start-BulkCompare $Provider -SelectedGroups $selectedGroups }
}
catch
{
$script:UIProvider.ShowMessageBox($_.Exception.Message, "Compare", "OK", "Error")
}
finally
{
Write-Status ""
}
})
$script:cbCompareProvider.Add_SelectionChanged({ Set-CompareProviderOptions $this })
Set-CompareProviderOptions $script:cbCompareProvider
$script:UIProvider.ShowModalForm("Bulk Compare Objects", $script:bulkCompareForm, $true)
}
function Update-BulkCompareFormForProvider
{
param($Provider)
if(-not $script:grdObjectsToCompareSection) { return }
$script:grdObjectsToCompareSection.Visibility = if($Provider -and $Provider.IgnoreGroups) { "Collapsed" } else { "Visible" }
}
# ─── Single Compare ──────────────────────────────────────────────────────────
function Show-GraphCompareForm
{
param([object[]]$Policies)
if(-not $Policies -or $Policies.Count -eq 0) { return }
$script:compareForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\CompareForm.xaml"), $true)
if(-not $script:compareForm) { return }
$script:compareSource = $Policies[0]
$script:compareTargetPolicy = if($Policies.Count -ge 2) { $Policies[1] } else { $null }
$script:compareInputPanel = $null
$script:compareInputPanelCache = @{}
$script:cbCompareType.ItemsSource = $script:comparisonTypes
$script:cbCompareType.SelectedValue = (Get-SettingStoreValue "Compare" "Type" "property")
$script:cbSingleCompareOutputFormat.ItemsSource = @($script:compareOutputProviders)
$script:cbSingleCompareOutputFormat.SelectedValue = (Get-SettingStoreValue "Compare" "SingleOutputFormat" "csv")
if($script:chkIgnoreCoreProperties)
{
$script:chkIgnoreCoreProperties.IsChecked = ((Get-SettingStoreValue "Compare" "IgnoreCoreProperties" "true") -eq "true")
}
Initialize-CompareSharedOptionControls
if($script:cbCompareFilter)
{
# 3-way result filter (All / Mismatch / Match).
$savedFilter = Get-SettingStoreValue "Compare" "ResultFilter" "All"
$script:cbCompareFilter.SelectedItem = @($script:cbCompareFilter.Items) | Where-Object { "$($_.Tag)" -eq "$savedFilter" } | Select-Object -First 1
if(-not $script:cbCompareFilter.SelectedItem) { $script:cbCompareFilter.SelectedIndex = 0 }
$script:cbCompareFilter.Add_SelectionChanged({ Update-CompareGridFilter })
}
$script:txtIntuneObject.Text = $Policies[0].Name
$compareInputTypes = @(
[PSCustomObject]@{ Name = "File"; Value = "file"; OptionsXaml = "CompareFileOptions" }
[PSCustomObject]@{ Name = "Intune Object"; Value = "intune"; OptionsXaml = "CompareIntuneObjectOptions" }
)
$script:cbCompareInput.ItemsSource = $compareInputTypes
$script:cbCompareInput.Add_SelectionChanged({ Set-CompareInputOptions $this })
$script:cbCompareInput.SelectedValue = if($script:compareTargetPolicy) { "intune" } else { (Get-SettingStoreValue "Compare" "InputType" "file") }
$script:UIProvider.AddXamlEvent($script:compareForm, "btnClose", "add_click", {
$script:compareForm = $null
$script:compareSource = $null
$script:compareTargetPolicy = $null
$script:compareInputPanel = $null
$script:compareInputPanelCache = $null
$script:chkIgnoreCoreProperties = $null
$script:cbCompareFilter = $null
$script:chkSkipCompareAssignments = $null
$script:cbCompareMultiValueDelimiter = $null
$script:txtCompareSummary = $null
$script:dgCompareInfo = $null
$script:UIProvider.ShowModalObject()
})
$script:UIProvider.AddXamlEvent($script:compareForm, "btnStartCompare", "add_click", {
Write-Status "Compare objects"
Save-SettingStoreValue "Compare" "Type" $script:cbCompareType.SelectedValue
Save-SettingStoreValue "Compare" "InputType" $script:cbCompareInput.SelectedValue
Save-SettingStoreValue "Compare" "SingleOutputFormat" $script:cbSingleCompareOutputFormat.SelectedValue
if($script:chkIgnoreCoreProperties)
{
Save-SettingStoreValue "Compare" "IgnoreCoreProperties" $(if($script:chkIgnoreCoreProperties.IsChecked) { "true" } else { "false" })
}
if($script:cbCompareFilter -and $script:cbCompareFilter.SelectedItem)
{
Save-SettingStoreValue "Compare" "ResultFilter" ([string]$script:cbCompareFilter.SelectedItem.Tag)
}
Save-CompareSharedOptionSettings
Set-CompareRuntimeOptionsFromUI
Start-CompareObjects
Write-Status ""
})
$script:UIProvider.AddXamlEvent($script:compareForm, "btnSwap", "add_click", {
if(-not $script:compareSource -or -not $script:compareTargetPolicy) { return }
$tmp = $script:compareSource
$script:compareSource = $script:compareTargetPolicy
$script:compareTargetPolicy = $tmp
$script:txtIntuneObject.Text = $script:compareSource.Name
if($script:compareInputPanel)
{
$txt = $script:compareInputPanel.FindName("txtCompareIntuneObject")
if($txt) { $txt.Text = $script:compareTargetPolicy.Name }
}
})
$script:UIProvider.AddXamlEvent($script:compareForm, "btnCompareSave", "add_click", {
if(($script:dgCompareInfo.ItemsSource | Measure-Object).Count -eq 0) { return }
$outProv = if($script:cbSingleCompareOutputFormat -and $script:cbSingleCompareOutputFormat.SelectedItem) {
$script:cbSingleCompareOutputFormat.SelectedItem
} else {
[CompareCSVOutputProvider]::new()
}
$sf = [System.Windows.Forms.SaveFileDialog]::new()
$sf.FileName = $script:compareSource.Name
$sf.DefaultExt = $outProv.Extension
$sf.FilterIndex = if($outProv.Extension -eq "json") { 2 } else { 1 }
$sf.Filter = "CSV files (*.csv)|*.csv|JSON files (*.json)|*.json|All files (*.*)|*.*"
$initialDir = Get-SettingStoreValue "Compare" "LastSaveDirectory"
if(-not $initialDir) { $initialDir = Get-SettingValue "RootFolder" }
if($initialDir) { $sf.InitialDirectory = $initialDir }
if($sf.ShowDialog() -eq "OK")
{
Save-SettingStoreValue "Compare" "LastSaveDirectory" ([IO.FileInfo]$sf.FileName).DirectoryName
$ext = [IO.Path]::GetExtension($sf.FileName).TrimStart(".")
$outProv = Get-CompareOutputProviderByExtension $ext
$props = Get-CompareOutputProps
$outProv.FormatRows($script:dgCompareInfo.ItemsSource, $props) | Out-File -LiteralPath $sf.FileName -Force -Encoding UTF8
}
})
$script:UIProvider.AddXamlEvent($script:compareForm, "btnCompareCopy", "add_click", {
Set-CompareRuntimeOptionsFromUI
(Get-CompareCsvInfo $script:dgCompareInfo.ItemsSource $script:compareSource) | Set-Clipboard
})
$script:UIProvider.ShowModalForm("Compare Intune Objects", $script:compareForm, $true)
}
function Set-CompareInputOptions
{
param($Control)
$InputType = $Control.SelectedItem
if(-not $InputType) { return }
$Panel = $null
if($script:compareInputPanelCache -is [Hashtable] -and $script:compareInputPanelCache.ContainsKey($InputType.Value))
{
$Panel = $script:compareInputPanelCache[$InputType.Value]
}
elseif($InputType.OptionsXaml)
{
$Panel = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\$($InputType.OptionsXaml).xaml"))
if($Panel)
{
$script:compareInputPanelCache[$InputType.Value] = $Panel
Register-CompareInputBrowseEvents $Panel $InputType
}
}
$script:compareInputPanel = $Panel
$script:ccCompareInputOptions.Content = $Panel
if($Panel -and $InputType.Value -eq "intune" -and $script:compareTargetPolicy)
{
$txt = $Panel.FindName("txtCompareIntuneObject")
if($txt) { $txt.Text = $script:compareTargetPolicy.Name }
}
}
function Register-CompareInputBrowseEvents
{
param($Panel, $InputType)
$browseFile = $Panel.FindName("browseCompareObject")
if($browseFile)
{
$browseFile.Tag = $Panel
$browseFile.Add_Click({
$p = $this.Tag
$path = Get-SettingStoreValue "" "LastUsedFullPath"
if($path) { $path = [IO.Directory]::GetParent($path).FullName }
if($script:compareSource -and $script:compareSource.PolicyType)
{
# No ?? operator here - this file must parse on PS5.1 too.
$typePath = [IO.Path]::Combine($(if($path) { $path } else { "" }), $script:compareSource.PolicyType.Folder)
if([IO.Directory]::Exists($typePath)) { $path = $typePath }
}
$lastFile = Get-SettingStoreValue "Compare" "LastFile"
if(-not [String]::IsNullOrEmpty($lastFile)) { $path = ([IO.FileInfo]$lastFile).DirectoryName }
$of = [System.Windows.Forms.OpenFileDialog]::new()
$of.Multiselect = $false
$of.Filter = "Json files (*.json)|*.json"
if($path) { $of.InitialDirectory = $path }
if($of.ShowDialog() -eq "OK")
{
$txt = $p.FindName("txtCompareFile")
if($txt) { $txt.Text = $of.FileName }
Save-SettingStoreValue "Compare" "LastFile" $of.FileName
}
})
}
$browseIntune = $Panel.FindName("browseIntuneObject")
if($browseIntune)
{
$browseIntune.Tag = $Panel
$browseIntune.Add_Click({
$p = $this.Tag
$sourceType = if($script:compareSource) { $script:compareSource.PolicyType } else { $null }
if(-not $sourceType) {
$script:UIProvider.ShowMessageBox("Select a source policy first.", "Compare", "OK", "Warning") | Out-Null
return
}
$sourceId = if($script:compareSource) { $script:compareSource.Id } else { $null }
# Seed with same-type policies already cached in the main view so the
# dialog opens with something in it and no Graph call. Searching or
# "Load all in scope" goes to Graph from there.
$initial = @()
if($script:intuneManagerPolicyCollection) {
$initial = @($script:intuneManagerPolicyCollection | Where-Object {
$_.PolicyType -and $_.PolicyType.ID -eq $sourceType.ID -and
(-not $sourceId -or $_.Id -ne $sourceId)
})
}
$exclude = @()
if($sourceId) { $exclude = @($sourceId) }
$selected = Show-PolicySearchDialog `
-Title "Select Intune Object to Compare ($($sourceType.Title))" `
-DefaultPolicyTypeId $sourceType.ID `
-ExcludeIds $exclude `
-InitialItems $initial
if($selected)
{
$script:compareTargetPolicy = $selected
$txt = $p.FindName("txtCompareIntuneObject")
if($txt) { $txt.Text = $selected.Name }
}
})
}
}
function Start-CompareObjects
{
if(-not $script:compareSource) { return }
$compareMode = if($script:cbCompareInput) { $script:cbCompareInput.SelectedValue } `
elseif($script:compareTargetPolicy) { "intune" } `
else { "file" }
if($compareMode -eq "intune")
{
if(-not $script:compareTargetPolicy)
{
$script:UIProvider.ShowMessageBox("No Intune object selected for comparison", "Compare", "OK", "Error")
return
}
Write-Status "Compare Intune objects"
Resolve-FullPolicyForCompare $script:compareSource
Resolve-FullPolicyForCompare $script:compareTargetPolicy
$compareResult = Compare-PolicyObjects @($script:compareSource, $script:compareTargetPolicy)
$script:dgCompareInfo.ItemsSource = $compareResult
Update-CompareGridFilter
Update-CompareSummary
$tabCtrl = $script:compareForm.FindName("tabCompare")
if($tabCtrl) { $tabCtrl.SelectedIndex = 1 }
Write-Status ""
return
}
$compareFile = ""
if($script:compareInputPanel)
{
$txtFile = $script:compareInputPanel.FindName("txtCompareFile")
if($txtFile) { $compareFile = $txtFile.Text }
}
if(-not $compareFile)
{
$script:UIProvider.ShowMessageBox("No file selected for comparison", "Compare", "OK", "Error")
return
}
if(-not [IO.File]::Exists($compareFile))
{
$script:UIProvider.ShowMessageBox("File '$compareFile' not found", "Compare", "OK", "Error")
return
}
try
{
$filePolicy = $script:compareSource.PolicyType.GetObject([IO.FileInfo]$compareFile)
if(-not $filePolicy)
{
$script:UIProvider.ShowMessageBox("Failed to load file '$compareFile'. Object type may not match.", "Compare", "OK", "Error")
return
}
}
catch
{
$script:UIProvider.ShowMessageBox("Failed to load file '$compareFile': $($_.Exception.Message)", "Compare", "OK", "Error")
return
}
Resolve-FullPolicyForCompare $script:compareSource
if($script:compareSource.Object.'@OData.Type' -ne $filePolicy.Object.'@OData.Type')
{
if(($script:UIProvider.ShowMessageBox("The object types do not match.`n`nDo you want to compare the objects anyway?", "Compare", "YesNo", "Warning")) -ne "Yes")
{
return
}
}
$compareResult = Compare-PolicyObjects @($script:compareSource, $filePolicy)
$script:dgCompareInfo.ItemsSource = $compareResult
Update-CompareGridFilter
Update-CompareSummary
$tabCtrl = $script:compareForm.FindName("tabCompare")
if($tabCtrl) { $tabCtrl.SelectedIndex = 1 }
Write-Status ""
}
function Update-CompareSummary
{
if(-not $script:txtCompareSummary -or -not $script:dgCompareInfo) { return }
$items = @($script:dgCompareInfo.ItemsSource)
$total = $items.Count
$mismatches = @($items | Where-Object { $_.Match -eq $false }).Count
$skipped = @($items | Where-Object { $null -eq $_.Match }).Count
$matched = $total - $mismatches - $skipped
$script:txtCompareSummary.Text = "$total properties - $matched matched, $mismatches mismatched$(if($skipped -gt 0) { ", $skipped ignored" } else { '' })"
}
function Update-CompareGridFilter
{
if(-not $script:dgCompareInfo -or -not $script:dgCompareInfo.Items) { return }
$mode = if($script:cbCompareFilter -and $script:cbCompareFilter.SelectedItem) { [string]$script:cbCompareFilter.SelectedItem.Tag } else { "All" }
switch($mode)
{
"Mismatch" { $script:dgCompareInfo.Items.Filter = [Predicate[object]]{ param($item) $item.Match -eq $false } }
"Match" { $script:dgCompareInfo.Items.Filter = [Predicate[object]]{ param($item) $item.Match -eq $true } }
default { $script:dgCompareInfo.Items.Filter = $null }
}
}
# ─── Provider Options ─────────────────────────────────────────────────────────
function Set-CompareProviderOptions
{
param($Control)
$Provider = $Control.SelectedItem
$providerPanel = $null
if($Provider -and $Provider.OptionsXaml)
{
# The cached panels are keyed by tenant so a tenant switch rebuilds them.
# No current provider caches tenant data on its panel; the invalidation
# stays as the only guard should a future one do so.
if($script:CompareProviderOptionsCache -isnot [Hashtable] -or
$script:CompareProviderOptionsCacheTenant -ne $script:OrganizationId)
{
$script:CompareProviderOptionsCache = @{}
$script:CompareProviderOptionsCacheTenant = $script:OrganizationId
}
if($script:CompareProviderOptionsCache.ContainsKey($Provider.Value))
{
$providerPanel = $script:CompareProviderOptionsCache[$Provider.Value]
}
else
{
$providerPanel = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\$($Provider.OptionsXaml).xaml"))
if($providerPanel)
{
$script:CompareProviderOptionsCache[$Provider.Value] = $providerPanel
Register-CompareProviderBrowseEvents $providerPanel $Provider
}
else
{
Write-Log "Failed to load provider options for '$($Provider.Name)'" 3
}
}
if($providerPanel)
{
$providerPanel.DataContext = $Provider
}
$script:ccContentProviderOptions.Content = $providerPanel
}
else
{
$script:ccContentProviderOptions.Content = $null
}
$script:ccContentProviderOptions.Visibility = if($null -eq $script:ccContentProviderOptions.Content) { "Collapsed" } else { "Visible" }
Update-BulkCompareFormForProvider $Provider
}
function Register-CompareProviderBrowseEvents
{
param($Panel, $Provider)
$browseExportPath = $Panel.FindName("browseExportPath")
if($browseExportPath)
{
$browseExportPath.Tag = @{ Provider = $Provider; Panel = $Panel }
$browseExportPath.Add_Click({
$tag = $this.Tag
$folder = $script:UIProvider.ShowFolderPicker($tag.Provider.ExportPath, "Select root folder for compare")
if($folder)
{
$tag.Provider.ExportPath = $folder
$dc = $tag.Panel.DataContext; $tag.Panel.DataContext = $null; $tag.Panel.DataContext = $dc
}
})
}
$browseSavePath = $Panel.FindName("browseSavePath")
if($browseSavePath)
{
$browseSavePath.Tag = @{ Provider = $Provider; Panel = $Panel }
$browseSavePath.Add_Click({
$tag = $this.Tag
$folder = $script:UIProvider.ShowFolderPicker($tag.Provider.SavePath, "Select save folder")
if($folder)
{
$tag.Provider.SavePath = $folder
$dc = $tag.Panel.DataContext; $tag.Panel.DataContext = $null; $tag.Panel.DataContext = $dc
}
})
}
$browseSource = $Panel.FindName("browseExportPathSource")
if($browseSource)
{
$browseSource.Tag = @{ Provider = $Provider; Panel = $Panel }
$browseSource.Add_Click({
$tag = $this.Tag
$folder = $script:UIProvider.ShowFolderPicker($tag.Provider.SourcePath, "Select source root folder")
if($folder)
{
$tag.Provider.SourcePath = $folder
$dc = $tag.Panel.DataContext; $tag.Panel.DataContext = $null; $tag.Panel.DataContext = $dc
}
})
}
$browseCompare = $Panel.FindName("browseExportPathCompare")
if($browseCompare)
{
$browseCompare.Tag = @{ Provider = $Provider; Panel = $Panel }
$browseCompare.Add_Click({
$tag = $this.Tag
$folder = $script:UIProvider.ShowFolderPicker($tag.Provider.ComparePath, "Select compare root folder")
if($folder)
{
$tag.Provider.ComparePath = $folder
$dc = $tag.Panel.DataContext; $tag.Panel.DataContext = $null; $tag.Panel.DataContext = $dc
}
})
}
}
@@ -0,0 +1,296 @@
# Per-view Copy dialog (Copy-IntuneManagerPolicy).
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Copy-IntuneManagerPolicy
{
if(-not $script:dgIntuneManagerObjects.SelectedItem)
{
$script:UIProvider.ShowMessageBox("No object selected`n`nSelect the $($script:IntuneManagerSelectedObject.Title) item you want to copy", "Error", "OK", "Error")
return
}
$script:copyForm = Initialize-Window ($script:AppUIRootFolder + "\Xaml\CopyDialog.xaml")
if(-not $script:copyForm) { return }
$newName = "$($script:dgIntuneManagerObjects.SelectedItem.Name) - Copy"
if($script:dgIntuneManagerObjects.SelectedItem.PolicyType.CopyDefaultName)
{
$newName = $script:dgIntuneManagerObjects.SelectedItem.PolicyType.CopyDefaultName
$script:dgIntuneManagerObjects.SelectedItem.PSObject.Properties | ForEach-Object { $newName = $newName -replace "%$($_.Name)%", $script:dgIntuneManagerObjects.SelectedItem."$($_.Name)" }
}
$script:UIProvider.SetXamlProperty($script:copyForm, "txtObjectName", "Text", $newName)
if($script:dgIntuneManagerObjects.SelectedItem.HasDescription -ne $false)
{
$script:UIProvider.SetXamlProperty($script:copyForm, "txtObjectDescription", "Text", $script:dgIntuneManagerObjects.SelectedItem.Description)
}
else
{
$script:UIProvider.SetXamlProperty($script:copyForm, "txtObjectDescription", "IsEnabled", $false)
}
$tokenList = @()
$tokenList += Get-TokenInfo | ForEach-Object {
$name = $_.TenantName
if($_.IsDefault) {
$name = $name + " (Current)"
}
$_ | Add-Member -NotePropertyName "TenantNameEx" -NotePropertyValue $name | Out-Null
$_
}
$script:UIProvider.SetXamlProperty($script:copyForm, "cbDestinationTenant", "ItemsSource", $tokenList)
$script:UIProvider.SetXamlProperty($script:copyForm, "cbDestinationTenant", "SelectedItem", ($tokenList | Where-Object IsDefault -eq $true))
# ---- Scope tags (dual-list) ----------------------------------------------
# Same pattern as the Details view, with two cross-cutting concerns:
# (1) destination tenant determines which tag list to show — scope tag IDs
# are per-tenant, so when the user picks a different tenant the list
# reloads from THAT tenant's dependency cache.
# (2) Assigned-list seed crosses tenants by NAME (source tag names that
# exist in the destination get pre-checked; the rest drop with a log).
$script:_copyScopeTagPropName = $null
$sourceItem = $script:dgIntuneManagerObjects.SelectedItem
if($sourceItem.PolicyType.ScopeTagProperty) {
$script:_copyScopeTagPropName = [string]$sourceItem.PolicyType.ScopeTagProperty
}
if(-not $script:_copyScopeTagPropName) {
$script:UIProvider.SetXamlProperty($script:copyForm, "pnlCopyScopeTagsLabel", "Visibility", "Collapsed")
$script:UIProvider.SetXamlProperty($script:copyForm, "grdCopyScopeTags", "Visibility", "Collapsed")
}
else {
# Capture the source tags by NAME so we can re-seed the Assigned list
# against the destination tenant whenever the tenant combo changes.
$script:_copySourceScopeTagNames = @()
$sourceTagIds = @()
if($sourceItem.Object -and $sourceItem.Object.PSObject.Properties[$script:_copyScopeTagPropName]) {
$sourceTagIds = @($sourceItem.Object.$script:_copyScopeTagPropName | ForEach-Object { [string]$_ })
}
try {
$srcDeps = Get-GraphDependencySourceObjects $sourceItem -DefaultPoliciesOnly
if($srcDeps -and $srcDeps.ContainsKey("ScopeTags")) {
foreach($id in $sourceTagIds) {
$tag = @($srcDeps["ScopeTags"]) | Where-Object { [string]$_.Id -eq $id } | Select-Object -First 1
if($tag) { $script:_copySourceScopeTagNames += [string]$tag.Name }
}
}
}
catch { Write-LogDebug "Source scope-tag name capture failed: $($_.Exception.Message)" }
$script:colCopyAvailableScopeTags = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
$script:colCopyAssignedScopeTags = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
$script:UIProvider.SetXamlProperty($script:copyForm, "lstCopyAvailableScopeTags", "ItemsSource", $script:colCopyAvailableScopeTags)
$script:UIProvider.SetXamlProperty($script:copyForm, "lstCopyAssignedScopeTags", "ItemsSource", $script:colCopyAssignedScopeTags)
# Single source of truth (same pattern as Details view): full list +
# assigned-ids set; ObservableCollections derived from both. Mutual
# exclusion is structural — no chance of a tag drifting between lists.
$script:_copyAllScopeTags = @()
$script:_copyAssignedIds = [System.Collections.Generic.HashSet[string]]::new()
$script:_syncCopyScopeTagLists = {
$script:colCopyAvailableScopeTags.Clear()
$script:colCopyAssignedScopeTags.Clear()
foreach($tag in $script:_copyAllScopeTags) {
if($script:_copyAssignedIds.Contains($tag.Id)) {
[void]$script:colCopyAssignedScopeTags.Add($tag)
} else {
[void]$script:colCopyAvailableScopeTags.Add($tag)
}
}
}
# Reload both lists for a given destination tenant. Cache lookup first;
# live fetch if cold. Re-seeds the assigned-ids set by name lookup so
# cross-tenant copies pre-pick the same-named tags in the destination.
# Dedupes by Id (the cache can hold both the synthetic Default and the
# real Default scope tag).
$script:_reloadCopyScopeTags = {
param([int]$DestTokenId)
$destTags = @()
try {
$destTokenInfo = Get-TokenInfo $DestTokenId
if($destTokenInfo) {
$cacheId = "DependencyObjects_$($destTokenInfo.TenantId)"
$deps = Get-CacheObject $cacheId
if($deps -and $deps.ContainsKey("ScopeTags")) {
$destTags = @($deps["ScopeTags"])
}
}
}
catch { Write-LogDebug "Copy scope tags: cache lookup failed: $($_.Exception.Message)" }
if($destTags.Count -eq 0) {
# Cold cache — fetch live for this tenant. ScopeTags is small; cheap.
try {
$destTags = @(Get-GraphPolicies -PolicyType "ScopeTags" -TokenId $DestTokenId)
# Mirror what Initialize-TenantDependencyCache does: prepend the
# synthetic Default so it's available for assignment.
$destTags = @([PSCustomObject]@{ ID = 0; Name = "Default" }) + $destTags
}
catch { Write-LogError "Failed to load scope tags for destination tenant" $_.Exception }
}
# Project + dedupe by Id.
$dedup = @()
$seenIds = [System.Collections.Generic.HashSet[string]]::new()
foreach($tag in $destTags) {
$idStr = [string]$tag.Id
if(-not $seenIds.Add($idStr)) { continue }
$dedup += [PSCustomObject]@{ Id = $idStr; Name = [string]$tag.Name }
}
$script:_copyAllScopeTags = @($dedup | Sort-Object Name)
# Re-seed assigned-ids from the captured source tag NAMES (cross-tenant
# safe). Names that don't exist in the destination simply don't get
# selected — the user can pick something else.
$script:_copyAssignedIds.Clear()
$seedNames = [System.Collections.Generic.HashSet[string]]::new()
foreach($n in $script:_copySourceScopeTagNames) { [void]$seedNames.Add($n) }
foreach($tag in $script:_copyAllScopeTags) {
if($seedNames.Contains($tag.Name)) { [void]$script:_copyAssignedIds.Add($tag.Id) }
}
& $script:_syncCopyScopeTagLists
}
# Initial load against the default-selected destination tenant.
$initialDest = $script:UIProvider.GetXamlProperty($script:copyForm, "cbDestinationTenant", "SelectedItem")
if($initialDest) { & $script:_reloadCopyScopeTags $initialDest.Id }
$script:UIProvider.AddXamlEvent($script:copyForm, "cbDestinationTenant", "Add_SelectionChanged", {
$newDest = $script:UIProvider.GetXamlProperty($script:copyForm, "cbDestinationTenant", "SelectedItem")
if($newDest) { & $script:_reloadCopyScopeTags $newDest.Id }
})
# Move = mutate the canonical assigned-ids set, then re-derive both lists.
$script:_moveCopyScopeTags = {
param([string]$Action, [string[]]$Ids)
if($Ids.Count -eq 0) { return }
if($Action -eq "assign") {
foreach($id in $Ids) { [void]$script:_copyAssignedIds.Add([string]$id) }
} else {
foreach($id in $Ids) { [void]$script:_copyAssignedIds.Remove([string]$id) }
}
& $script:_syncCopyScopeTagLists
}
$script:UIProvider.AddXamlEvent($script:copyForm, "btnCopyScopeTagAssign", "Add_Click", {
try {
$lb = $script:copyForm.FindName("lstCopyAvailableScopeTags")
if(-not $lb -or $lb.SelectedItems.Count -eq 0) { return }
$ids = @($lb.SelectedItems | ForEach-Object { [string]$_.Id })
& $script:_moveCopyScopeTags "assign" $ids
}
catch { Write-LogError "Copy scope-tag assign handler failed" $_.Exception }
})
$script:UIProvider.AddXamlEvent($script:copyForm, "btnCopyScopeTagUnassign", "Add_Click", {
try {
$lb = $script:copyForm.FindName("lstCopyAssignedScopeTags")
if(-not $lb -or $lb.SelectedItems.Count -eq 0) { return }
$ids = @($lb.SelectedItems | ForEach-Object { [string]$_.Id })
& $script:_moveCopyScopeTags "unassign" $ids
}
catch { Write-LogError "Copy scope-tag unassign handler failed" $_.Exception }
})
$script:UIProvider.AddXamlEvent($script:copyForm, "lstCopyAvailableScopeTags", "Add_MouseDoubleClick", {
try {
$lb = $script:copyForm.FindName("lstCopyAvailableScopeTags")
if(-not $lb -or $lb.SelectedItems.Count -eq 0) { return }
$ids = @($lb.SelectedItems | ForEach-Object { [string]$_.Id })
& $script:_moveCopyScopeTags "assign" $ids
}
catch { Write-LogError "Copy scope-tag double-click (available) failed" $_.Exception }
})
$script:UIProvider.AddXamlEvent($script:copyForm, "lstCopyAssignedScopeTags", "Add_MouseDoubleClick", {
try {
$lb = $script:copyForm.FindName("lstCopyAssignedScopeTags")
if(-not $lb -or $lb.SelectedItems.Count -eq 0) { return }
$ids = @($lb.SelectedItems | ForEach-Object { [string]$_.Id })
& $script:_moveCopyScopeTags "unassign" $ids
}
catch { Write-LogError "Copy scope-tag double-click (assigned) failed" $_.Exception }
})
}
$script:copyForm.Add_ContentRendered({
$txtName = $script:copyForm.FindName("txtObjectName")
if($txtName)
{
$txtName.SelectAll()
}
})
$script:UIProvider.AddXamlEvent($script:copyForm, "btnOk", "Add_Click", {
$script:copyForm.DialogResult = $true
})
$script:copyForm.Owner = $script:window
$script:copyForm.Icon = $script:Window.Icon
$ret = $script:copyForm.ShowDialog()
if($ret)
{
$txtDescrption = $null
$newName = $script:UIProvider.GetXamlProperty($script:copyForm, "txtObjectName", "Text")
if(-not $newName)
{
Write-Log "New name cannot be empty. Copy object skipped" 2
Write-Status ""
return
}
if(($script:UIProvider.GetXamlProperty($script:copyForm, "txtObjectDescription", "IsEnabled")) -eq $true)
{
$txtDescrption = $script:UIProvider.GetXamlProperty($script:copyForm, "txtObjectDescription", "Text")
}
# Export profile
Write-Status "Copy $($script:dgIntuneManagerObjects.SelectedItem.Name)"
#$tokenId = $script:dgIntuneManagerObjects.SelectedItem.TokenId
$selectedTenant = $script:UIProvider.GetXamlProperty($script:copyForm, "cbDestinationTenant", "SelectedItem")
$tokenId = $selectedTenant.Id
$copyArgs = @{
InputObject = $script:dgIntuneManagerObjects.SelectedItem
Name = $newName
Description = $txtDescrption
TokenId = $tokenId
}
# Forward the user's scope-tag picks (when the section was shown). Pass
# even an empty array so the new copy explicitly reflects user intent —
# CopyObject will override the cloned JSON only when this is non-null.
# Read from the canonical $script:_copyAssignedIds set (single source of
# truth) rather than the UI-derived ObservableCollection.
if($script:_copyScopeTagPropName -and $null -ne $script:_copyAssignedIds) {
$copyArgs['ScopeTagIds'] = @($script:_copyAssignedIds)
}
$newPolicies = Copy-GraphPolicy @copyArgs
if($newPolicies -and (Get-SettingValue "RefreshObjectsAfterCopy") -eq $true) {
Invoke-IntuneActivateObject $script:IntuneManagerSelectedObject
}
#if($script:dgIntuneManagerObjects.SelectedItem.CopyObject($newName, $txtDescrption, 0)) {
# if((Get-SettingValue "RefreshObjectsAfterCopy") -eq $true)
# {
# Invoke-IntuneActivateObject $script:IntuneManagerSelectedObject
# }
#}
Write-Status ""
}
$script:dgIntuneManagerObjects.Focus()
}
#region Events
@@ -0,0 +1,47 @@
# Per-view Delete confirmation (Remove-GraphObjectsUI).
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Remove-GraphObjectsUI
{
$policiesToRemove = @()
if(($script:dgIntuneManagerObjects.ItemsSource | Where-Object IsSelected -eq $true).Count -gt 0)
{
$policiesToRemove += $script:dgIntuneManagerObjects.ItemsSource | Where-Object IsSelected -eq $true
}
elseif($script:dgIntuneManagerObjects.SelectedItem)
{
$policiesToRemove += $script:dgIntuneManagerObjects.SelectedItem
}
if($policiesToRemove.Count -eq 0)
{
$script:UIProvider.ShowMessageBox("No object selected`n`nSelect items you want to delete", "Error", "OK", "Error")
return
}
if(($script:UIProvider.ShowMessageBox("Are you sure you want to delete $($policiesToRemove.Count) object(s)?`n`nEnvironment: $($script:OrganizationName)", "Delete Objects?", "YesNo", "Warning")) -ne "Yes")
{
return
}
$deletedPolicies = $policiesToRemove | Remove-GraphPolicy
if($deletedPolicies) {
$script:dgIntuneManagerObjects.ItemsSource
$deletedPolicies | ForEach-Object {
try {
$index = [Array]::IndexOf($script:dgIntuneManagerObjects.ItemsSource.Id, $_.Id)
if($index -gt -1) {
($script:dgIntuneManagerObjects.ItemsSource).RemoveAt($index)
}
}
catch { }
}
}
Write-Status ""
}
@@ -0,0 +1,670 @@
# Details / object-view dialog (Show-IntuneManagerDetailedView + helpers).
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Get-DetailsSettingsSignature
{
param($Form)
if(-not $Form) { return "" }
$scopeTagIds = @()
if($script:_scopeTagPropName -and $null -ne $script:_detailsAssignedIds) {
$scopeTagIds = @($script:_detailsAssignedIds | Sort-Object)
}
$descriptionEnabled = $false
try { $descriptionEnabled = [bool]($script:UIProvider.GetXamlProperty($Form, "txtObjectDescription", "IsEnabled")) } catch {}
$signature = [PSCustomObject]@{
Name = [string]($script:UIProvider.GetXamlProperty($Form, "txtObjectName", "Text"))
DescriptionEnabled = $descriptionEnabled
Description = if($descriptionEnabled) { [string]($script:UIProvider.GetXamlProperty($Form, "txtObjectDescription", "Text")) } else { $null }
ScopeTagProperty = [string]$script:_scopeTagPropName
ScopeTagIds = $scopeTagIds
}
return ($signature | ConvertTo-Json -Depth 10 -Compress)
}
function Update-DetailsColumnEditorBindings
{
param($Form)
if(-not $Form) { return }
foreach($controlName in @("txtObjectColumnsProperty", "txtObjectColumnsHeader", "chkObjectColumnOverride")) {
try {
$ctl = $Form.FindName($controlName)
if(-not $ctl) { continue }
$expression = $ctl.GetBindingExpression([System.Windows.Controls.TextBox]::TextProperty)
if(-not $expression -and $controlName -eq "chkObjectColumnOverride") {
$expression = $ctl.GetBindingExpression([System.Windows.Controls.Primitives.ToggleButton]::IsCheckedProperty)
}
if($expression) { $expression.UpdateSource() }
}
catch { }
}
}
function Get-DetailsColumnsSignature
{
param($Form)
if(-not $Form) { return "" }
Update-DetailsColumnEditorBindings -Form $Form
$columns = @()
foreach($col in @($script:colObjectProperties)) {
if(-not $col) { continue }
$columns += [PSCustomObject]@{
Property = [string]$col.Property
Header = [string]$col.Header
}
}
$signature = [PSCustomObject]@{
OverrideDefaultColumns = [bool]($script:UIProvider.GetXamlProperty($Form, "chkObjectColumnOverride", "IsChecked"))
Columns = $columns
}
return ($signature | ConvertTo-Json -Depth 20 -Compress)
}
function Test-DetailsViewHasUnsavedChanges
{
param($Form)
if(-not $Form) { return $false }
$settingsDirty = $false
$columnsDirty = $false
try {
if($script:_detailsInitialSettingsSignature) {
$settingsDirty = ((Get-DetailsSettingsSignature -Form $Form) -ne $script:_detailsInitialSettingsSignature)
}
}
catch { Write-LogDebug "Details settings dirty check failed: $($_.Exception.Message)" }
try {
if($script:_detailsInitialColumnsSignature) {
$columnsDirty = ((Get-DetailsColumnsSignature -Form $Form) -ne $script:_detailsInitialColumnsSignature)
}
}
catch { Write-LogDebug "Details columns dirty check failed: $($_.Exception.Message)" }
return ($settingsDirty -or $columnsDirty)
}
function Confirm-DetailsViewClose
{
if(-not (Test-DetailsViewHasUnsavedChanges -Form $script:detailsForm)) { return $true }
$result = $script:UIProvider.ShowMessageBox(
"You have unsaved changes in Settings or Columns.`n`nClose without saving?",
"Unsaved changes", "YesNo", "Warning")
return ($result -eq "Yes")
}
function Show-IntuneManagerDetailedView
{
param(
$FormTitle = "",
[switch]$NoLoadFull)
$selectedItem = $script:dgIntuneManagerObjects.SelectedItem
if(-not $selectedItem -and $script:dgIntuneManagerObjects.ItemsSource) {
$selectedItem = @($script:dgIntuneManagerObjects.ItemsSource |
Where-Object { $_.PSObject.Properties['IsSelected'] -and $_.IsSelected -eq $true } |
Select-Object -First 1)
if($selectedItem.Count -gt 0) { $selectedItem = $selectedItem[0] }
}
if(-not $selectedItem) { return }
if(-not $selectedItem.JsonObject) { return }
$script:detailsForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\ObjectDetails.xaml"))
if(-not $script:detailsForm) { return }
$script:detailsForm.Tag = $selectedItem
$script:detailsForm | Add-Member -MemberType ScriptMethod -Name ConfirmClose -Value { Confirm-DetailsViewClose } -Force
if(-not $FormTitle) { $FormTitle = $selectedItem.PolicyName }
$objName = $selectedItem.Name
if($objName)
{
$FormTitle = "$FormTitle - $objName"
}
if($selectedItem.PolicyType.AddUIDetailsExtension) {
$selectedItem.PolicyType.AddUIDetailsExtension($script:detailsForm)
}
$script:UIProvider.SetXamlProperty($script:detailsForm, "txtValue", "Text", $selectedItem.JsonString)
if($selectedItem.PolicyType.AllowFullDetails -eq $false)
{
$script:UIProvider.SetXamlProperty($script:detailsForm, "btnFull", "Visibility", "Collapsed")
}
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnCopy", "Add_Click", {
$tmp = $script:detailsForm.FindName("txtValue")
if($tmp.Text) { $tmp.Text | Set-Clipboard }
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnFull", "Add_Click", {
$selectedItem = $script:detailsForm.Tag
if(-not $selectedItem) { return }
Write-Status "Get full object $($selectedItem.Name)"
[void]$selectedItem.Get()
if($selectedItem.IsFullObject)
{
$script:UIProvider.SetXamlProperty($script:detailsForm, "txtValue", "Text", $selectedItem.JsonString)
}
Write-Status ""
})
#Settings tab
$script:UIProvider.SetXamlProperty($script:detailsForm, "txtObjectName", "Text", $selectedItem.Name)
$script:UIProvider.SetXamlProperty($script:detailsForm, "txtObjectDescription", "Text", $selectedItem.Description)
# Snapshots for the save-confirmation dialog: it names the object and says
# exactly what changed, computed against these open-time values. The
# scope-tag pair resets here because the picker block below only runs for
# types WITH a scope-tag property - without the reset a previous dialog's
# sets would leak into this object's change summary.
$script:_detailsOriginalName = [string]$selectedItem.Name
$script:_detailsOriginalDesc = [string]$selectedItem.Description
$script:_detailsOriginalAssignedIds = $null
# ---- Scope tags (dual-list) ----------------------------------------------
# Type-level capability: _ScopeTagProperty defaults to "roleScopeTagIds" on every
# IntunePolicyType, but a few types clear it. Whether the property currently exists
# on the JSON is irrelevant — a user can assign tags to a policy that has none yet.
$script:_scopeTagPropName = $null
if($selectedItem.PolicyType.ScopeTagProperty) {
$script:_scopeTagPropName = [string]$selectedItem.PolicyType.ScopeTagProperty
}
if(-not $script:_scopeTagPropName) {
$script:UIProvider.SetXamlProperty($script:detailsForm, "pnlScopeTagsLabel", "Visibility", "Collapsed")
$script:UIProvider.SetXamlProperty($script:detailsForm, "grdScopeTags", "Visibility", "Collapsed")
}
else {
# Source the full tag list from the warm dependency cache
# (Initialize-TenantDependencyCache preloads it on auth). The cache list
# includes the synthetic Default (Id=0). Falls back to a live call if the
# cache is empty (e.g. a session that started before the preload landed).
$allScopeTags = @()
try {
$depObjects = Get-GraphDependencySourceObjects $selectedItem -DefaultPoliciesOnly
if($depObjects -and $depObjects.ContainsKey("ScopeTags")) {
$allScopeTags = @($depObjects["ScopeTags"])
}
}
catch { Write-LogDebug "Scope tag dependency cache lookup failed: $($_.Exception.Message)" }
if($allScopeTags.Count -eq 0) {
$allScopeTags = @(Get-GraphPolicies -PolicyType "ScopeTags" -TokenId $selectedItem._TokenId)
}
# Resolve currently-assigned IDs from the JSON. Stringify so the comparison
# works regardless of whether the cache entry stores Id as int (synthetic
# Default) or string (real tags from Graph). Safe when the property is
# absent — PowerShell yields $null which the empty-array cast swallows.
$assignedIdSet = [System.Collections.Generic.HashSet[string]]::new()
$currentIds = $null
if($selectedItem.Object -and
$selectedItem.Object.PSObject.Properties[$script:_scopeTagPropName]) {
$currentIds = $selectedItem.Object.$script:_scopeTagPropName
}
foreach($id in @($currentIds)) {
if($null -ne $id) { [void]$assignedIdSet.Add([string]$id) }
}
# Project to plain PSCustomObject {Id, Name} so the ListBox binding works
# uniformly. The cache mixes two shapes: the synthetic Default is a real
# PSCustomObject (renders fine), but real tags are IntunePolicyBase class
# instances with Name added as a ScriptProperty via Add-Member — WPF's
# DisplayMemberPath reflection on class instances doesn't reliably see
# ScriptProperties, so those rows render blank. Projecting normalizes both.
#
# Dedup by Id while projecting. The dependency cache can contain BOTH the
# synthetic Default and the real Default scope tag from Graph (both have
# Id "0"); without dedup, "Default" rendered twice and a single Assigned
# move could leave a duplicate stuck in the wrong list.
$script:_detailsAllScopeTags = @()
$seenIds = [System.Collections.Generic.HashSet[string]]::new()
foreach($tag in $allScopeTags) {
$idStr = [string]$tag.Id
if(-not $seenIds.Add($idStr)) { continue }
$script:_detailsAllScopeTags += [PSCustomObject]@{ Id = $idStr; Name = [string]$tag.Name }
}
$script:_detailsAllScopeTags = @($script:_detailsAllScopeTags | Sort-Object Name)
# Single source of truth for which tags are currently assigned. Both
# ObservableCollections are derived from this set + the full list, so
# mutual exclusion is structural — Available is always "everything that
# isn't in the assigned set", no chance of duplicate or drift bugs.
$script:_detailsAssignedIds = [System.Collections.Generic.HashSet[string]]::new()
foreach($id in $assignedIdSet) { [void]$script:_detailsAssignedIds.Add($id) }
$script:_detailsOriginalAssignedIds = [System.Collections.Generic.HashSet[string]]::new($script:_detailsAssignedIds)
$script:colAvailableScopeTags = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
$script:colAssignedScopeTags = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
$script:_syncDetailsScopeTagLists = {
$script:colAvailableScopeTags.Clear()
$script:colAssignedScopeTags.Clear()
foreach($tag in $script:_detailsAllScopeTags) {
if($script:_detailsAssignedIds.Contains($tag.Id)) {
[void]$script:colAssignedScopeTags.Add($tag)
} else {
[void]$script:colAvailableScopeTags.Add($tag)
}
}
}
& $script:_syncDetailsScopeTagLists
$script:UIProvider.SetXamlProperty($script:detailsForm, "lstAvailableScopeTags", "ItemsSource", $script:colAvailableScopeTags)
$script:UIProvider.SetXamlProperty($script:detailsForm, "lstAssignedScopeTags", "ItemsSource", $script:colAssignedScopeTags)
# Click handlers reference only $script:-scoped state so they keep access
# to module-private helpers (Get-XamlProperty etc.). Move = mutate the
# canonical assigned-ids set, then re-derive both lists.
$script:_moveScopeTags = {
param([string]$Action, [string[]]$Ids)
if($Ids.Count -eq 0) { return }
if($Action -eq "assign") {
foreach($id in $Ids) { [void]$script:_detailsAssignedIds.Add([string]$id) }
} else {
foreach($id in $Ids) { [void]$script:_detailsAssignedIds.Remove([string]$id) }
}
& $script:_syncDetailsScopeTagLists
}
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnScopeTagAssign", "Add_Click", {
try {
$lb = $script:detailsForm.FindName("lstAvailableScopeTags")
if(-not $lb -or $lb.SelectedItems.Count -eq 0) {
Write-Status "Select one or more scope tag(s) in the Available list first"
return
}
$ids = @($lb.SelectedItems | ForEach-Object { [string]$_.Id })
& $script:_moveScopeTags "assign" $ids
}
catch { Write-LogError "Scope-tag assign handler failed" $_.Exception }
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnScopeTagUnassign", "Add_Click", {
try {
$lb = $script:detailsForm.FindName("lstAssignedScopeTags")
if(-not $lb -or $lb.SelectedItems.Count -eq 0) {
Write-Status "Select one or more scope tag(s) in the Assigned list first"
return
}
$ids = @($lb.SelectedItems | ForEach-Object { [string]$_.Id })
& $script:_moveScopeTags "unassign" $ids
}
catch { Write-LogError "Scope-tag unassign handler failed" $_.Exception }
})
# Double-click toggles. Lets the user assign/unassign without aiming for
# the arrow buttons, which is the more common interaction pattern.
$script:UIProvider.AddXamlEvent($script:detailsForm, "lstAvailableScopeTags", "Add_MouseDoubleClick", {
try {
$ids = @(@($script:UIProvider.GetXamlProperty($script:detailsForm, "lstAvailableScopeTags", "SelectedItems")) | ForEach-Object { [string]$_.Id })
& $script:_moveScopeTags "assign" $ids
}
catch { Write-LogError "Scope-tag double-click (available) failed" $_.Exception }
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "lstAssignedScopeTags", "Add_MouseDoubleClick", {
try {
$ids = @(@($script:UIProvider.GetXamlProperty($script:detailsForm, "lstAssignedScopeTags", "SelectedItems")) | ForEach-Object { [string]$_.Id })
& $script:_moveScopeTags "unassign" $ids
}
catch { Write-LogError "Scope-tag double-click (assigned) failed" $_.Exception }
})
}
$script:_detailsInitialSettingsSignature = Get-DetailsSettingsSignature -Form $script:detailsForm
# ToDo: Disable description if property does not exist
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnObjectSettingsSave", "Add_Click", {
$selectedItem = $script:detailsForm.Tag
if(-not $selectedItem) { return }
# Name the object and say what changed - computed against the
# open-time snapshots so the dialog is meaningful.
$originalName = if($script:_detailsOriginalName) { $script:_detailsOriginalName } else { [string]$selectedItem.Name }
$newName = [string]($script:UIProvider.GetXamlProperty($script:detailsForm, "txtObjectName", "Text"))
$newDesc = [string]($script:UIProvider.GetXamlProperty($script:detailsForm, "txtObjectDescription", "Text"))
$changes = @()
if($newName -and $newName -cne $originalName) { $changes += "New name: '$newName'" }
if($newDesc -cne [string]$script:_detailsOriginalDesc) { $changes += "Description updated" }
if($null -ne $script:_detailsAssignedIds -and $null -ne $script:_detailsOriginalAssignedIds)
{
$added = @($script:_detailsAssignedIds | Where-Object { -not $script:_detailsOriginalAssignedIds.Contains($_) }).Count
$removed = @($script:_detailsOriginalAssignedIds | Where-Object { -not $script:_detailsAssignedIds.Contains($_) }).Count
if($added -or $removed) { $changes += "Scope tags: $added added, $removed removed" }
}
$confirmMsg = "Are you sure you want to update '$originalName'?"
if($changes.Count) { $confirmMsg += "`n`n" + ($changes -join "`n") }
if(($script:UIProvider.ShowMessageBox($confirmMsg, "Update object information?", "YesNo", "Warning")) -ne "Yes")
{
return
}
Write-Status "Update object information for $($selectedItem.Name)"
$nameValue = ($script:UIProvider.GetXamlProperty($script:detailsForm, "txtObjectName", "Text"))
if(-not $nameValue)
{
$script:UIProvider.ShowMessageBox("Name property must not be empty!", "Error", "OK", "Error")
return
}
# Save settings here...
$nameProp = (?? $selectedItem.PolicyType.NameProperty "displayName")
$idProp = (?? $selectedItem.PolicyType.IDProperty "id")
$updateHT = @{}
$updateHT.Add($idProp, $selectedItem.ID)
$updateHT.Add($nameProp, $nameValue)
if(($selectedItem.Object."@odata.type"))
{
$updateHT.Add("@odata.type", ($selectedItem.JsonObject."@odata.type"))
}
if(($script:UIProvider.GetXamlProperty($script:detailsForm, "txtObjectDescription", "IsEnabled")) -eq $true)
{
$updateHT.Add("description", ($script:UIProvider.GetXamlProperty($script:detailsForm, "txtObjectDescription", "Text")))
if($null -eq $updateHT["description"] -and $selectedItem.Description)
{
# If description is null, remove it
$updateHT["description"] = ""
}
elseif($null -eq $updateHT["description"])
{
$updateHT.Remove("description")
}
}
# Scope tags: write the assigned-list IDs back under the policy's
# ScopeTagProperty (typically roleScopeTagIds). Read from the canonical
# $script:_detailsAssignedIds set (single source of truth) — the
# ObservableCollection is just a UI derivative.
if($script:_scopeTagPropName -and $null -ne $script:_detailsAssignedIds) {
$assignedIds = @($script:_detailsAssignedIds)
# Soft guard: Intune treats roleScopeTagIds as containing "0" (Default)
# by default. A non-empty list that omits Default is unusual and often
# rejected by Graph; warn before committing rather than letting the
# PATCH 400 silently. Empty list isn't flagged — that reads as a
# deliberate reset.
if($assignedIds.Count -gt 0 -and $assignedIds -notcontains "0") {
$proceed = $script:UIProvider.ShowMessageBox(
"The 'Default' scope tag (Id 0) is not in the assigned list.`n`nIntune usually requires Default to be present and the API call may fail.`n`nContinue anyway?",
"Default scope tag missing", "YesNo", "Warning")
if($proceed -ne "Yes") {
Write-Status ""
return
}
}
$updateHT.Add($script:_scopeTagPropName, $assignedIds)
}
$updateObj = [PSCustomObject]$updateHT
$api = "$($selectedItem.PolicyType.API)/$($selectedItem.ID)"
$json = $updateObj | ConvertTo-Json -Depth 20
$ret = Invoke-MSGraphAPI $api -HttpMethod "PATCH" -Content $json -FullResponseObject -TokenId $selectedItem._TokenId
Write-Status ""
if($ret.Success -eq $false)
{
Write-log "Failed to update object information updated for $($selectedItem.Name). Error: $($ret.StatusCode) - $($ret.StatusDescription)"
$script:UIProvider.ShowMessageBox("Object information could not be verified!`n`nCheck the log file", "Update warning", "OK", "Warning")
}
else {
Write-log "Object information updated for $($selectedItem.Name)"
# Refresh the in-memory object so the parent grid's "ScopeTags" column
# reflects what we just sent. Clearing _ScopeTags forces the lazy
# getter to re-resolve from the dependency cache next access; updating
# Object.<prop> keeps subsequent reads (and a re-open of this dialog)
# in sync without a round-trip. Items.Refresh() then asks WPF to
# re-read every bound cell — without it, the DataGrid keeps showing
# the pre-Save snapshot because PSCustomObject doesn't notify.
if($script:_scopeTagPropName -and $null -ne $script:_detailsAssignedIds) {
$assignedIds = @($script:_detailsAssignedIds)
try {
$selectedItem.Object.$script:_scopeTagPropName = $assignedIds
} catch { Write-LogDebug "Failed to update in-memory ScopeTags array: $($_.Exception.Message)" }
$selectedItem._ScopeTags = $null
$selectedItem._ScopeTagsString = $null
}
# Name / description may also have changed in this Save — refresh once
# at the end so every cell of the affected row re-reads, not just the
# ScopeTags column.
try { $script:dgIntuneManagerObjects.Items.Refresh() }
catch { Write-LogDebug "DataGrid refresh failed: $($_.Exception.Message)" }
$script:_detailsInitialSettingsSignature = Get-DetailsSettingsSignature -Form $script:detailsForm
}
})
#Columns tab
$skipBasicProperties = @("JsonObject", "Object", "JsonString", "TenantId", "TokenId", "IsSelected")
$arrBasicProperties = @()
foreach($prop in ($selectedItem.PSObject.Properties | Where-Object Name -notin $skipBasicProperties))
{
$arrBasicProperties += ([PSCustomObject]@{ Name=$prop.Name;Value=$prop;Source="Basic" })
}
$arrBasicProperties = $arrBasicProperties | Sort-Object -Property Name
$script:UIProvider.SetXamlProperty($script:detailsForm, "lstBasicProperties", "ItemsSource", $arrBasicProperties)
$arrObjectProperties = @()
$skipObjectProperties = @("@odata.editLink")
foreach($prop in ($selectedItem.Object.PSObject.Properties | Where-Object { $_.Name -notin $skipObjectProperties -and $_.Name -notlike "*?@odata.*" }))
{
$arrObjectProperties += ([PSCustomObject]@{ Name=$prop.Name;Value=$prop;Source="Object" })
}
$arrObjectProperties = $arrObjectProperties | Sort-Object -Property Name
$script:UIProvider.SetXamlProperty($script:detailsForm, "lstObjectProperties", "ItemsSource", $arrObjectProperties)
$script:colObjectProperties = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
$script:UIProvider.SetXamlProperty($script:detailsForm, "lstObjectColumns", "ItemsSource", $script:colObjectProperties)
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnObjectColumnsReset", "Add_Click", {
<#
if(([System.Windows.MessageBox]::Show("Are you sure you want to reset columns to default?", "Reset Columns?", "YesNo", "Warning")) -ne "Yes")
{
return
}
$selectedType = Get-SelectedObjectTypeString
Remove-SettingStoreValue "IntuneManager\ObjectColumns\$selectedType" "$($script:IntuneManagerSelectedObject.Id)"
#>
$script:colObjectProperties.Clear()
Show-ObjectDefaultColumnsSettings -Reset
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "lstObjectColumns", "Add_SelectionChanged", {
$script:UIProvider.SetXamlProperty($script:detailsForm, "grdObjectColumns", "DataContext", ($script:UIProvider.GetXamlProperty($script:detailsForm, "lstObjectColumns", "SelectedItem")))
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnBasicColumnsAdd", "Add_Click", {
$selectedItem = $script:UIProvider.GetXamlProperty($script:detailsForm, "lstBasicProperties", "SelectedItem")
if($selectedItem) {
$script:colObjectProperties.Add(([ObjectColumnInfo]::new($selectedItem.Name, "")))
}
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnObjectColumnsAdd", "Add_Click", {
$selectedItem = $script:UIProvider.GetXamlProperty($script:detailsForm, "lstObjectProperties", "SelectedItem")
if($selectedItem) {
$script:colObjectProperties.Add(([ObjectColumnInfo]::new("Object." + $selectedItem.Name, "")))
}
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnObjectColumnsMoveUp", "Add_Click", {
$selectedIndex = $script:UIProvider.GetXamlProperty($script:detailsForm, "lstObjectColumns", "SelectedIndex")
if($selectedIndex -gt 0)
{
$tmpObj = $script:colObjectProperties[$selectedIndex]
$script:colObjectProperties.RemoveAt($selectedIndex)
$tmpObj = $script:colObjectProperties.Insert(($selectedIndex-1),$tmpObj)
$script:UIProvider.SetXamlProperty($script:detailsForm, "lstObjectColumns", "SelectedIndex", ($selectedIndex-1))
}
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnObjectColumnsMoveDown", "Add_Click", {
$selectedIndex = $script:UIProvider.GetXamlProperty($script:detailsForm, "lstObjectColumns", "SelectedIndex")
if($selectedIndex -ge 0 -and $selectedIndex -lt ($script:colObjectProperties.Count-1)) {
$tmpObj = $script:colObjectProperties[$selectedIndex]
$script:colObjectProperties.RemoveAt($selectedIndex)
$tmpObj = $script:colObjectProperties.Insert(($selectedIndex+1),$tmpObj)
$script:UIProvider.SetXamlProperty($script:detailsForm, "lstObjectColumns", "SelectedIndex", ($selectedIndex+1))
}
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnObjectColumnsDelete", "Add_Click", {
$selectedIndex = $script:UIProvider.GetXamlProperty($script:detailsForm, "lstObjectColumns", "SelectedIndex")
if($selectedIndex -ge 0) {
if(($script:UIProvider.ShowMessageBox("Are you sure you want to remove selected column?", "Remove Columns?", "YesNo", "Warning")) -ne "Yes")
{
return
}
$script:colObjectProperties.RemoveAt($selectedIndex)
}
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnObjectColumnsClear", "Add_Click", {
if(($script:UIProvider.ShowMessageBox("Are you sure you want to clear custom column settings?", "Clear Custom Columns?", "YesNo", "Warning")) -ne "Yes") {
return
}
$script:colObjectProperties.Clear()
})
$script:UIProvider.AddXamlEvent($script:detailsForm, "btnObjectColumnsSave", "Add_Click", {
$selectedType = Get-SelectedObjectTypeString
if(-not $selectedType) { return }
if(($script:UIProvider.ShowMessageBox("Are you sure you want to save custom column settings?", "Save Custom Columns?", "YesNo", "Warning")) -ne "Yes")
{
return
}
if($script:colObjectProperties.Count -gt 0)
{
$arrCols = @()
if(($script:UIProvider.GetXamlProperty($script:detailsForm, "chkObjectColumnOverride", "IsChecked")) -eq $true)
{
$arrCols += "0"
}
foreach($colProp in $script:colObjectProperties)
{
$tmp = $colProp.Property
if($colProp.Header -and $colProp.Header -cne $colProp.Property)
{
$tmp = "$($tmp)=$($colProp.Header)"
}
$arrCols += $tmp
}
$strCols = $arrCols -join ","
Save-SettingStoreValue "IntuneManager\ObjectColumns\$selectedType" "$($script:IntuneManagerSelectedObject.Id)" $strCols
}
else
{
$strCols = $null
Remove-SettingStoreValue "IntuneManager\ObjectColumns\$selectedType" "$($script:IntuneManagerSelectedObject.Id)"
}
Show-ObjectDefaultColumnsSettings
$script:_detailsInitialColumnsSignature = Get-DetailsColumnsSignature -Form $script:detailsForm
})
Show-ObjectDefaultColumnsSettings
$script:_detailsInitialColumnsSignature = Get-DetailsColumnsSignature -Form $script:detailsForm
# Show dialog
$script:UIProvider.ShowModalForm($FormTitle, $detailsForm)
}
function Show-ObjectDefaultColumnsSettings
{
param([switch]$Reset)
if($Reset -ne $true) {
$selectedType = Get-SelectedObjectTypeString
if(-not $selectedType) { return }
$strColSettings = Get-SettingStoreValue "IntuneManager\ObjectColumns\$selectedType" "$($script:IntuneManagerSelectedObject.Id)"
}
else {
$strColSettings = ""
}
$script:colObjectProperties.Clear()
$defaultColumns = $script:IntuneManagerSelectedObject.ViewProperties
if($strColSettings)
{
$arrColSettings += $strColSettings -split ",|;"
$script:UIProvider.SetXamlProperty($script:detailsForm, "chkObjectColumnOverride", "IsChecked", ($arrColSettings.Count -gt 0 -and $arrColSettings[0] -eq "0"))
$script:UIProvider.SetXamlProperty($script:detailsForm, "lblObjectColumnsConfig", "Text", $strColSettings)
$start = 0
if($arrColSettings.Count -gt 0 -and ($arrColSettings[0] -eq "0" -or $arrColSettings[0] -eq "1"))
{
$start++
}
$colArr = @()
for($i = $start;$i -lt $arrColSettings.Count;$i++)
{
$colProp,$colHeader= $arrColSettings[$i].Split("=")
if(-not $colHeader)
{
$colHeader = $colProp
}
$script:colObjectProperties.Add([ObjectColumnInfo]::new($colProp,$colHeader))
$colArr += $colProp
}
if(($arrColSettings.Count -eq 0 -or $arrColSettings[0] -ne "0"))
{
$tmpArr = $defaultColumns
$tmpArr += $colArr
$colArr = $tmpArr
}
$script:UIProvider.SetXamlProperty($script:detailsForm, "lblObjectColumnsConfig", "Text", ("$(($colArr-join ','))"))
}
else
{
$script:UIProvider.SetXamlProperty($script:detailsForm, "lblObjectColumnsConfig", "Text", "$(($defaultColumns -join ',')) (Default)")
}
}
# Match-resolution helpers (Get-IntuneImportPolicyReferenceTokens,
# Normalize-IntuneImportPolicyName, New-IntuneImportMatchResult,
# Resolve-IntuneImportUpdateTarget) moved to Internal/IntuneManager.ps1
# so the Avalonia tree can share them.
@@ -0,0 +1,73 @@
# Per-view Export dialog (single-policy/single-type export).
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Show-IntuneManagerExportForm
{
$script:exportForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\ExportForm.xaml"))
if(-not $script:exportForm) { return }
$script:UIProvider.SetXamlProperty($script:exportForm, "txtExportPath", "Text", (?? (Get-SettingStoreValue "" "LastUsedRoot") (Get-SettingValue "RootFolder")))
$exportSettings = [IntuneManagerExportSettings]::new()
$policyTypes = Get-IntuneManagerSelectedPolicyTypes
$policyTypes | Add-IntuneManagerExportProperties -ExportSettings $exportSettings
$policyTypes | Add-IntuneManagerExportUIExtensions -Form $script:exportForm
$script:exportForm.DataContext = $exportSettings
$script:UIProvider.SetXamlProperty($script:exportForm, "btnExportSelected", "IsEnabled", ($null -ne $script:dgIntuneManagerObjects.SelectedItem))
if(($script:dgIntuneManagerObjects.ItemsSource | Where-Object IsSelected -eq $true).Count -gt 0)
{
$script:UIProvider.SetXamlProperty($script:exportForm, "lblSelectedObject", "Content", "$(($script:dgIntuneManagerObjects.ItemsSource | Where-Object IsSelected -eq $true).Count) selected object(s)")
}
elseif($script:dgIntuneManagerObjects.SelectedItem)
{
$script:UIProvider.SetXamlProperty($script:exportForm, "lblSelectedObject", "Content", "Selected object: $($script:dgIntuneManagerObjects.SelectedItem.Name)")
}
$script:UIProvider.AddXamlEvent($script:exportForm, "btnCancel", "add_click", {
$script:exportForm = $null
Show-ModalObject
})
$script:UIProvider.AddXamlEvent($script:exportForm, "btnExportSelected", "add_click", {
$selectedItems = @()
if(($script:dgIntuneManagerObjects.ItemsSource | Where-Object IsSelected -eq $true).Count -gt 0) {
$selectedItems += $script:dgIntuneManagerObjects.ItemsSource | Where-Object IsSelected -eq $true
}
else {
$selectedItems += $script:dgIntuneManagerObjects.SelectedItem
}
$selectedItems | Export-GraphPolicy -ExportSettings $script:exportForm.DataContext
$script:exportForm.DataContext.Save()
$script:exportForm = $null
Show-ModalObject
})
$script:UIProvider.AddXamlEvent($script:exportForm, "btnExportAll", "add_click", {
$script:dgIntuneManagerObjects.ItemsSource | Export-GraphPolicy -ExportSettings $script:exportForm.DataContext
$script:exportForm = $null
Show-ModalObject
})
$script:UIProvider.AddXamlEvent($script:exportForm, "browseExportPath", "add_click", {
$folder = Get-Folder ($script:UIProvider.GetXamlProperty($script:exportForm, "txtExportPath", "Text")) "Select root folder for export"
if($folder)
{
$script:exportForm.DataContext.ExportFolder = $folder
$tmp = $script:exportForm.DataContext
$script:exportForm.DataContext = $null
$script:exportForm.DataContext = $tmp
}
})
$script:UIProvider.ShowModalForm("Export $($script:IntuneManagerSelectedObject.Title) objects", $script:exportForm, $true)
}
@@ -0,0 +1,54 @@
# Add-IntuneManagerExportUIExtensions / Add-IntuneManagerImportUIExtensions iterators.
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Add-IntuneManagerExportUIExtensions
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[IntunePolicyTypeBase[]]
$InputObject,
[Parameter(Mandatory = $true)]
[Object]
$Form
)
Begin { Write-Log "Add Export UI Extensions - Start" }
Process {
foreach($policyType in $InputObject) {
if($policyType.AddUIExportExtensions) {
$policyType.AddUIExportExtensions($Form)
}
}
}
End { Write-Log "Add Export UI Extensions - Done" }
}
function Add-IntuneManagerImportUIExtensions
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[IntunePolicyTypeBase[]]
$InputObject,
[Parameter(Mandatory = $true)]
[Object]
$Form
)
Begin { Write-Log "Add Import UI Extensions - Start" }
Process {
foreach($policyType in $InputObject) {
if($policyType.AddUIImportExtensions) {
$policyType.AddUIImportExtensions($Form)
}
}
}
End { Write-Log "Add Import UI Extensions - Done" }
}
@@ -0,0 +1,338 @@
# Per-view Import dialog (single-policy/single-folder import). Includes the kept-on-purpose `<# ToDo #>` Get-PoliciesFromFile stub block.
# Split out of IntuneManagerUIWPF.ps1 on 2026-06-03 to match the Avalonia tree's
# per-feature layout (architecture rule R9 — keep the WPF shell from growing further).
function Update-IntuneImportMatchInfo
{
if(-not $script:DGObjectsToImport -or -not $script:DGObjectsToImport.ItemsSource) { return }
$importType = $script:importForm.DataContext.ImportType
$sameTenant = $script:importForm.DataContext.SameTenant
$existingPolicies = @($script:dgIntuneManagerObjects.ItemsSource)
foreach($item in @($script:DGObjectsToImport.ItemsSource)) {
if(-not $item -or -not $item.Object) { continue }
$match = Resolve-IntuneImportUpdateTarget -ImportPolicy $item.Object -ExistingPolicies $existingPolicies -SameTenant $sameTenant -ImportType $importType
$item | Add-Member -MemberType NoteProperty -Name "ImportAction" -Value $match.Action -Force
$item | Add-Member -MemberType NoteProperty -Name "ImportMatch" -Value $match.Message -Force
$item | Add-Member -MemberType NoteProperty -Name "ImportMatchStrategy" -Value $match.Strategy -Force
$item | Add-Member -MemberType NoteProperty -Name "ImportTarget" -Value $match.Target -Force
}
try { $script:DGObjectsToImport.Items.Refresh() } catch { }
}
function Show-IntuneManagerImportForm
{
$policyTypes = Get-IntuneManagerSelectedPolicyTypes
if(($policyTypes | Measure-Object).Count -eq 0) {
$script:UIProvider.ShowMessageBox("No objects selected.", "Error", "OK", "Error")
return
}
$script:importForm = $script:UIProvider.GetXamlObject(($script:AppUIRootFolder + "\Xaml\ImportForm.xaml"))
if(-not $script:importForm) { return }
Write-Status "Load import form"
$script:UIProvider.SetXamlProperty($script:importForm, "cbImportType", "ItemsSource", $script:IntuneManagerImportOptions)
$importSettings = [IntuneManagerImportSettings]::new()
$policyTypes | Add-IntuneManagerImportProperties -ImportSettings $importSettings
#!!!$policyTypes | Add-IntuneManagerImportUIExtensions -Form $script:importForm
$script:importForm.DataContext = $importSettings
$path = Get-SettingStoreValue "" "LastUsedFullPath"
if($path)
{
$di = [IO.DirectoryInfo]$path
if(($policyTypes | Where-Object { $_.Folder -eq $di.Name})) {
$path = $di.Parent.FullName
}
if([IO.Directory]::Exists($path) -eq $false)
{
$path = Get-SettingStoreValue "" "LastUsedRoot"
}
$importSettings.ImportFolder = $path
}
$script:DGObjectsToImport = $script:importForm.FindName("dgObjectsToImport")
$column = Get-GridCheckboxColumn "Selected"
$script:DGObjectsToImport.Columns.Add($column)
$column.Header.IsChecked = $true # All items are checked by default
$column.Header.add_Click({
foreach($Item in $script:DGObjectsToImport.ItemsSource)
{
$Item.Selected = $this.IsChecked
}
$script:DGObjectsToImport.Items.Refresh()
}
)
# Add Object type column
$binding = [System.Windows.Data.Binding]::new("Object.Name")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Object Name"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:DGObjectsToImport.Columns.Add($column)
$binding = [System.Windows.Data.Binding]::new("Object.PolicyName")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Policy Type"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:DGObjectsToImport.Columns.Add($column)
$binding = [System.Windows.Data.Binding]::new("Object.PolicyBaseName")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Policy Base"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:DGObjectsToImport.Columns.Add($column)
$binding = [System.Windows.Data.Binding]::new("Object.Platform")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Platform"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:DGObjectsToImport.Columns.Add($column)
$binding = [System.Windows.Data.Binding]::new("Object.FileInfo.Name")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "File Name"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:DGObjectsToImport.Columns.Add($column)
$binding = [System.Windows.Data.Binding]::new("ImportAction")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Action"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:DGObjectsToImport.Columns.Add($column)
$binding = [System.Windows.Data.Binding]::new("ImportMatch")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Match"
$column.IsReadOnly = $true
$column.Binding = $binding
$script:DGObjectsToImport.Columns.Add($column)
$script:UIProvider.AddXamlEvent($script:importForm, "cbImportType", "Add_SelectionChanged", {
if($script:importForm -and $script:importForm.DataContext) {
$selectedValue = $script:UIProvider.GetXamlProperty($script:importForm, "cbImportType", "SelectedValue")
if($selectedValue) { $script:importForm.DataContext.ImportType = $selectedValue }
}
Update-IntuneImportMatchInfo
})
$script:UIProvider.AddXamlEvent($script:importForm, "browseImportFolder", "add_click", {
$folder = Get-Folder ($script:UIProvider.GetXamlProperty($script:importForm, "txtImportFolder", "Text")) "Select root folder for import"
if($folder)
{
$script:UIProvider.SetXamlProperty($script:importForm, "txtImportFolder", "Text", $folder)
$script:importForm.DataContext.ImportFolder = $folder
Get-ImportPoliciesFromFolder $folder
}
})
$script:UIProvider.AddXamlEvent($script:importForm, "btnCancel", "add_click", {
$script:importForm = $null
Show-ModalObject
})
$script:UIProvider.AddXamlEvent($script:importForm, "btnImportSelected", "add_click", {
Write-Status "Import selected objects"
# Honour the ClearCacheBeforeExportImport setting (bit 8 = manual import).
Invoke-GraphCacheClearBeforeOperation -Operation ManualImport
$importType = $script:importForm.DataContext.ImportType
# Persist the per-import toggles so the import pipeline (Get-SettingValue) honours them.
$script:importForm.DataContext.Save()
$selectedPolicies = $script:DGObjectsToImport.ItemsSource | Where-Object Selected -eq $true
$policiesToImport = @()
$updatedPolicies = @()
Update-IntuneImportMatchInfo
foreach ($selectedPolicy in @($selectedPolicies))
{
$importPolicy = $selectedPolicy.Object
$match = Resolve-IntuneImportUpdateTarget -ImportPolicy $importPolicy -ExistingPolicies $script:dgIntuneManagerObjects.ItemsSource -SameTenant $script:importForm.DataContext.SameTenant -ImportType $importType
if($match.Action -eq "Update" -and $importType -eq "update")
{
$updatedPolicy = $importPolicy.UpdateObject($match.Target, (Get-DefaultTokenId))
if($updatedPolicy) { $updatedPolicies += $updatedPolicy }
}
elseif($match.Action -eq "Replace")
{
$replacedPolicy = Invoke-IntuneImportReplace -ImportPolicy $importPolicy -Target $match.Target -ImportType $importType
if($replacedPolicy) { $updatedPolicies += $replacedPolicy }
}
elseif($match.Action -eq "Ambiguous")
{
Write-Log "Skip import/update for $($importPolicy.Name) ($($importPolicy.PolicyName)): $($match.Message)" 2
}
elseif($match.Action -eq "Skip")
{
Write-Log "Skip import/update for $($importPolicy.Name) ($($importPolicy.PolicyName)): $($match.Message)"
}
else {
$policiesToImport += $importPolicy
}
}
$importedPolicies = $policiesToImport | Import-GraphPolicy
if($importedPolicies.Count -gt 0 -or $updatedPolicies.Count -gt 0) {
Invoke-IntuneActivateObject $script:IntuneManagerSelectedObject | Out-Null
}
Show-ModalObject
Write-Status ""
})
$script:UIProvider.AddXamlEvent($script:importForm, "btnGetFiles", "add_click", {
# Used when the user manually updates the path and the press Get Files
Get-ImportPoliciesFromFolder $script:importForm.DataContext.ImportFolder
})
if($script:importForm.DataContext.ImportFolder)
{
Get-ImportPoliciesFromFolder $script:importForm.DataContext.ImportFolder -KeepStatus
}
$script:importForm.add_Loaded({
Write-Status ""
})
$script:UIProvider.ShowModalForm("Import objects", $script:importForm, $true)
}
function Get-ImportPoliciesFromFolder
{
param($Folder, [switch]$KeepStatus)
Write-Status "Get policy objects from $Folder"
$Folder = Expand-FileName $Folder
if([IO.Directory]::Exists($Folder) -eq $false) { return }
$params = @{}
$curPolicyTypes = Get-IntuneManagerSelectedPolicyTypes
$subFolders = @()
foreach($curPolicyType in $curPolicyTypes) {
if([IO.Directory]::Exists(([IO.Path]::Combine($Folder, $curPolicyType.Folder)))) {
$subFolders += $curPolicyType.Folder
}
}
if($subFolders.Count -gt 0) {
$params.Add("SubFolders", $subFolders)
}
if($curPolicyTypes) {
$params.Add("PolicyTypes", $curPolicyTypes)
}
#$params.Add("SearchSubFolders", $true) #!!!!
$items = @()
# !!! ToDo: Delete
#@(Get-PoliciesFromFile $folder @params) | ForEach-Object { $items += ([PSCustomObject]$_) }
@(Get-PoliciesFromFolder $folder @params) | ForEach-Object {
$policyObject = New-Object PSObject -Property @{
Selected = $true
Object = ([PSCustomObject]$_)
ImportAction = ""
ImportMatch = ""
ImportMatchStrategy = ""
ImportTarget = $null
}
$items += $policyObject
}
$script:DGObjectsToImport.ItemsSource = @($items | Sort-Object { $_.Object.Name })
Save-SettingStoreValue "" "LastUsedFullPath" $Folder
$migrationInfo, $sameTenant = Get-MigrationTableInfo $Folder $script:organizationId
$script:importForm.DataContext.SameTenant = [bool]$sameTenant
$script:UIProvider.SetXamlProperty($script:importForm, "chkReplaceDependencyIDs", "IsEnabled", ($sameTenant -eq $false))
$script:UIProvider.SetXamlProperty($script:importForm, "chkReplaceDependencyIDs", "IsChecked", ($sameTenant -eq $false))
$script:UIProvider.SetXamlProperty($script:importForm, "lblMigrationTableInfo", "Content", $migrationInfo)
Update-IntuneImportMatchInfo
if($KeepStatus -ne $true) {
Write-Status ""
}
}
<#
### ToDo: Delete !!!
function Get-PoliciesFromFile
{
param($Path, $Exclude = @("*_settings.json","*_assignments.json", "MigrationTable*.json"), $SelectedStatus = $true, $SubFolders = @(), [switch]$SearchSubFolders, [int]$Depth = 0)
if(-not $Path -or (Test-Path $Path -PathType Container) -eq $false) { return }
$params = @{}
if($Exclude)
{
$params.Add("Exclude", $Exclude)
}
if($SearchSubFolders -eq $true) {
$params.Add("Recurse", $true)
}
if($Depth -gt 0) {
$params.Add("Depth", $Depth)
}
$fileArr = @()
$policyTypes = Get-IntuneManagerSelectedPolicyTypes
$paths = @()
if($SubFolders.Count -gt 0) {
Get-ChildItem -path $Path | Where-Object { $_.Name -in $SubFolders -and $_ -is [IO.DirectoryInfo] } | ForEach-Object { $paths += $_.FullName }
}
else {
$paths += $path
}
foreach($policyPath in $paths) {
foreach($file in (Get-ChildItem -path "$policyPath\*.json" @params))
{
$graphObj = Get-GraphPolicyFromFile $file $policyTypes
if(-not $graphObj) { continue }
$policyObject = New-Object PSObject -Property @{
#FileName = $file.Name
#FileInfo = $file
Selected = $SelectedStatus
Object = ([PSCustomObject]$graphObj)
}
$fileArr += $policyObject
}
}
return @($fileArr)
}
#>
@@ -0,0 +1,118 @@
# Scoped policy picker (WPF).
#
# Thin wrapper over Show-ObjectPickerDialog: it supplies the tenant list, the
# scope list and a search handler, so the dialog queries Graph for the typed
# name inside the selected policy group / policy type instead of enumerating the
# whole tenant up front. The search itself lives in Internal/PolicySearch.ps1
# and is shared with the Avalonia backend (R10 - this file only collects input
# and renders).
function Show-PolicySearchDialog
{
param(
[string]$Title = "Select policy",
# Preselects this policy type in the "Search in" dropdown.
[string]$DefaultPolicyTypeId,
# Ids to keep out of the results (e.g. the policy being compared).
[string[]]$ExcludeIds,
# Rows to show before the first search. The compare flows seed this
# from the main view's already-loaded list so the dialog opens with
# something useful in it.
[object[]]$InitialItems
)
$scopes = Get-PolicySearchScopes -DefaultPolicyTypeId $DefaultPolicyTypeId
if(@($scopes).Count -eq 0) {
$script:UIProvider.ShowMessageBox("No policy types are available to search.", "Select policy", "OK", "Warning") | Out-Null
return $null
}
$defaultKey = Get-PolicySearchDefaultScopeKey -Scopes $scopes -PolicyTypeId $DefaultPolicyTypeId
# Empty unless more than one tenant is signed in; the dialog hides the combo
# in that case and every search runs against the default token.
$tenants = Get-PolicySearchTenants
$defaultTenantKey = Get-PolicySearchDefaultTenantKey -Tenants $tenants
$cols = @(
[PSCustomObject]@{ Header = "Name"; Binding = "Name" }
[PSCustomObject]@{ Header = "Type"; Binding = "PolicyType.Title" }
)
# Both handlers read $script: state rather than capturing locals - see the
# $script:_pickerState note in Show-ObjectPickerDialog.
$script:_policySearchState = @{
ExcludeIds = $ExcludeIds
MinLength = Get-PolicySearchMinLength
}
$searchHandler = {
param($SearchText, $Scope)
$st = $script:_policySearchState
if(-not $Scope) { return @() }
if([String]::IsNullOrWhiteSpace($SearchText) -or $SearchText.Trim().Length -lt $st.MinLength) {
Set-PickerStatusText "Type at least $($st.MinLength) characters, or use 'Load all in scope'."
return @()
}
# $null when the dialog is single-tenant, in which case the default
# token is used.
$tenant = Get-PickerSelectedTenant
$tokenId = if($tenant) { [int]$tenant.TokenId } else { Get-DefaultTokenId }
$where = if($tenant) { " in $($tenant.Title)" } else { "" }
Write-Status "Searching $($Scope.Title.Trim())$where..."
try {
$found = @(Search-IntunePolicies -Scope $Scope -SearchText $SearchText.Trim() -ExcludeIds $st.ExcludeIds -TokenId $tokenId)
if($found.Count -eq 0) {
Set-PickerStatusText "No object in $($Scope.Title.Trim())$where matches '$($SearchText.Trim())'."
}
else {
Set-PickerStatusText "$($found.Count) object(s) found$where."
}
return $found
}
finally {
Write-Status ""
}
}
$loadHandler = {
$st = $script:_policySearchState
$scope = Get-PickerSelectedScope
if(-not $scope) { return @() }
$tenant = Get-PickerSelectedTenant
$tokenId = if($tenant) { [int]$tenant.TokenId } else { Get-DefaultTokenId }
$where = if($tenant) { " in $($tenant.Title)" } else { "" }
Write-Status "Loading all objects in $($scope.Title.Trim())$where..."
try {
$loaded = @(Search-IntunePolicies -Scope $scope -ExcludeIds $st.ExcludeIds -TokenId $tokenId)
Set-PickerStatusText "$($loaded.Count) object(s) in $($scope.Title.Trim())$where."
return $loaded
}
finally {
Write-Status ""
}
}
try {
return Show-ObjectPickerDialog `
-Title $Title `
-Items $InitialItems `
-DisplayColumns $cols `
-Scopes $scopes `
-SelectedScopeKey $defaultKey `
-Tenants $tenants `
-SelectedTenantKey $defaultTenantKey `
-SearchHandler $searchHandler `
-LoadHandler $loadHandler `
-LoadLabel "Load all in scope"
}
finally {
$script:_policySearchState = $null
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
# Render parsed Markdown into a WPF TextBlock.
#
# The parse lives in Internal/MarkdownSimple.ps1 and is shared with Avalonia
# (R10); this file only turns blocks into WPF inlines, so the toolkit types stay
# inside UI/WPF (R12). The Avalonia counterpart is
# UI/Avalonia/Extensions/MarkdownRenderAvalonia.ps1 and is deliberately a
# near-mirror of this - same block handling, different inline construction.
#
# Everything goes into a single TextBlock rather than a document/panel per block:
# Run exposes FontSize, FontWeight, FontFamily and Background, so headings, bold
# spans, code and indented bullets all fit in one control, and the existing
# ScrollViewer keeps working unchanged.
function Set-WpfMarkdownText {
[CmdletBinding()]
param(
[Parameter(Mandatory)] $TextBlock,
[string]$Markdown
)
if (-not $TextBlock) { return }
$TextBlock.Inlines.Clear()
if ([string]::IsNullOrEmpty($Markdown)) { return }
$baseSize = if ($TextBlock.FontSize -gt 0) { [double]$TextBlock.FontSize } else { 12.0 }
$style = Get-SimpleMarkdownStyle
$codeBrush = New-Object System.Windows.Media.SolidColorBrush ([System.Windows.Media.ColorConverter]::ConvertFromString($style.CodeBackground))
$codeFont = New-Object System.Windows.Media.FontFamily ($style.CodeFontFamily)
foreach ($block in (ConvertFrom-SimpleMarkdown $Markdown)) {
if ($block.Kind -eq 'Blank') {
$TextBlock.Inlines.Add((New-Object System.Windows.Documents.LineBreak))
continue
}
# Lead-in for list items and quotes. Leading whitespace in a Run is
# preserved, so indentation works without a panel per level.
$lead = switch ($block.Kind) {
'ListItem' { (' ' * ($block.Indent * 4)) + $block.Marker + ' ' }
'Quote' { $style.QuoteMarker }
default { $null }
}
if ($lead) {
$TextBlock.Inlines.Add((New-Object System.Windows.Documents.Run -ArgumentList $lead))
}
foreach ($inline in $block.Inlines) {
if ($inline.LineBreak) {
$TextBlock.Inlines.Add((New-Object System.Windows.Documents.LineBreak))
# Keep continuation lines of a bullet aligned under its text.
if ($block.Kind -eq 'ListItem') {
$pad = ' ' * (($block.Indent * 4) + $block.Marker.Length + 1)
$TextBlock.Inlines.Add((New-Object System.Windows.Documents.Run -ArgumentList $pad))
}
continue
}
# Links become real Hyperlinks; RequestNavigate opens the default
# browser, matching how the About and Welcome dialogs handle theirs.
if ($inline.Url) {
$link = New-Object System.Windows.Documents.Hyperlink
$link.Inlines.Add((New-Object System.Windows.Documents.Run -ArgumentList ([string]$inline.Text)))
try { $link.NavigateUri = New-Object System.Uri ($inline.Url) } catch { }
$link.Add_RequestNavigate({ Open-ExternalUri $_.Uri.AbsoluteUri; $_.Handled = $true })
if ($inline.Bold) { $link.FontWeight = [System.Windows.FontWeights]::Bold }
$TextBlock.Inlines.Add($link)
continue
}
$run = New-Object System.Windows.Documents.Run -ArgumentList ([string]$inline.Text)
if ($inline.Bold -or $block.Kind -eq 'Heading') {
$run.FontWeight = [System.Windows.FontWeights]::Bold
}
if ($block.Kind -eq 'Heading') {
$run.FontSize = Get-SimpleMarkdownHeadingSize -Level $block.Level -BaseSize $baseSize
}
if ($inline.Code -or $block.Kind -eq 'CodeBlock') {
$run.FontFamily = $codeFont
$run.Background = $codeBrush
}
if ($block.Kind -eq 'Quote') {
$run.FontStyle = [System.Windows.FontStyles]::Italic
}
$TextBlock.Inlines.Add($run)
}
$TextBlock.Inlines.Add((New-Object System.Windows.Documents.LineBreak))
}
}
+145
View File
@@ -0,0 +1,145 @@
# Text columns for WPF DataGrids: one line per row, capped length, pixel scrolling.
#
# A DataGridTextColumn renders every line of a multi-line value, so a store app
# description makes its row forty lines tall and turns the grid's row-based
# scrolling into a lurch. TextWrapping=NoWrap does not help: it only stops soft
# wrapping, and hard line breaks still render. And a single very long line has
# the opposite problem - it widens its column until the others are pushed out of
# view. FirstLineConverter trims the bound value to its first non-blank line,
# caps it at MaxLength characters (0 = no cap), appends an ellipsis when either
# happened, and with ConverterParameter "Full" returns the whole text only when
# something was cut - so the tooltip appears exactly on the cells that hide it.
if(-not ("FirstLineConverter" -as [type]))
{
# References resolved from loaded types, so the same call compiles on
# .NET Framework (PS5.1) and .NET (PS7).
Add-Type -ReferencedAssemblies @(
[System.Windows.Data.IValueConverter].Assembly.Location, # PresentationFramework
[System.Windows.DependencyObject].Assembly.Location, # WindowsBase
[System.Windows.Markup.MarkupExtension].Assembly.Location # System.Xaml - Binding.DoNothing needs it on .NET Framework
) -TypeDefinition @'
using System;
using System.Globalization;
using System.Windows.Data;
public class FirstLineConverter : IValueConverter
{
// Maximum characters kept from the first line. 0 = unlimited.
public int MaxLength { get; set; }
// First non-blank line, capped at maxLength; "\u2026" appended when any
// later non-blank line exists or the cap cut something off.
public static string FirstLine(string text, int maxLength, out bool trimmed)
{
trimmed = false;
if (text == null) return null;
string[] lines = text.Split(new[] { '\r', '\n' });
string first = null;
foreach (string line in lines)
{
string t = line.Trim();
if (t.Length == 0) continue;
if (first == null) { first = t; continue; }
trimmed = true;
break;
}
if (first == null) return string.Empty;
if (maxLength > 0 && first.Length > maxLength)
{
// Prefer a word boundary when one falls in the second half of the
// cap, so the cell ends "...built in" rather than "...with Cop".
int cut = first.LastIndexOf(' ', maxLength);
if (cut < maxLength / 2) cut = maxLength;
first = first.Substring(0, cut).TrimEnd();
trimmed = true;
}
return trimmed ? first + "\u2026" : first;
}
// A binding to a PowerShell SCRIPT property (the row's Description, Platform,
// LastModified ...) delivers the value wrapped in a PSObject; a note property
// (Object.description) delivers the raw string. Unwrap by reflection so the
// converter sees the string either way without referencing the PowerShell
// assembly - this is what left Description rendering all its lines while
// Object.description was trimmed.
private static object Unwrap(object value)
{
if (value == null) return null;
var t = value.GetType();
if (t.FullName != "System.Management.Automation.PSObject") return value;
var p = t.GetProperty("BaseObject");
return p == null ? value : p.GetValue(value, null);
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
value = Unwrap(value);
string text = value as string;
if (text == null) return value; // dates, numbers: leave the column's own formatting alone
bool trimmed;
string first = FirstLine(text, MaxLength, out trimmed);
if (parameter != null && string.Equals(parameter.ToString(), "Full", StringComparison.OrdinalIgnoreCase))
{
return trimmed ? text : null; // tooltip only where something was hidden
}
return first;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
'@
}
# Builds a read-only text column. With -FirstLineOnly the cell shows the value's
# first line (capped at -MaxLength characters when that is above 0) and carries
# the full text as a tooltip. Sorting and the grid filter still use the
# underlying value: both read the binding path, not what is displayed.
function New-GridTextColumn
{
param(
[Parameter(Mandatory = $true)][string]$Path,
[string]$Header,
[switch]$FirstLineOnly,
[int]$MaxLength = 0
)
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = if($Header) { $Header } else { $Path.Split('.')[-1] }
$column.IsReadOnly = $true
$binding = [System.Windows.Data.Binding]::new($Path)
if($FirstLineOnly)
{
$binding.Converter = [FirstLineConverter]::new()
$binding.Converter.MaxLength = $MaxLength
$tooltip = [System.Windows.Data.Binding]::new($Path)
$tooltip.Converter = [FirstLineConverter]::new()
$tooltip.Converter.MaxLength = $MaxLength
$tooltip.ConverterParameter = 'Full'
$style = [System.Windows.Style]::new([System.Windows.Controls.TextBlock])
$style.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.TextBlock]::TextTrimmingProperty, [System.Windows.TextTrimming]::CharacterEllipsis))
$style.Setters.Add([System.Windows.Setter]::new([System.Windows.FrameworkElement]::ToolTipProperty, $tooltip))
$column.ElementStyle = $style
}
$column.Binding = $binding
return $column
}
# Pixel-based scrolling for a DataGrid built in code; the XAML grids declare
# VirtualizingPanel.ScrollUnit="Pixel" directly. WPF's default scrolls by whole
# rows and snaps the top row to the edge, which lurches as soon as rows differ in
# height. Row virtualization stays on.
function Set-GridPixelScrolling
{
param([Parameter(Mandatory = $true)]$Grid)
[System.Windows.Controls.VirtualizingPanel]::SetScrollUnit($Grid, [System.Windows.Controls.ScrollUnit]::Pixel)
[System.Windows.Controls.VirtualizingPanel]::SetVirtualizationMode($Grid, [System.Windows.Controls.VirtualizationMode]::Recycling)
}
+17
View File
@@ -0,0 +1,17 @@
# Null-free array for anything assigned to a WPF ItemsSource.
#
# A $null element in an ItemsControl kills the host: WPF's layout and automation
# passes index the items, and both throw ArgumentNullException on a null one, on
# every render, until the process dies. @($null) is a ONE-element array containing
# null, so an omitted parameter or an empty result lands there via the usual @($x).
# Full write-up and the regression proof: Tests/UIItemsArray.Tests.ps1.
#
# The leading comma is load-bearing. A bare `return @(...)` is unrolled by the
# pipeline, so zero items would come back as $null and ONE item as the bare
# object - which cannot be assigned to an ItemsSource at all.
function Get-UIItemsArray
{
param($Items)
return ,@(@($Items) | Where-Object { $null -ne $_ })
}
+23
View File
@@ -0,0 +1,23 @@
# Menu icons load when a menu is built, not when the module is imported.
#
# ClassExtensions/IntuneCommonUIExtensions.ps1 attaches LoadIconImage/GetImage
# to every policy-type and group singleton at import time (the closures must
# capture the module-bound XAML loader then). It used to call LoadIconImage
# right away too, which parsed 82 icon files through XamlReader before Start.ps1
# had even reached Show-MainWindow - the whole pass ran before the splash
# existed. Show-ViewMenu calls this instead, once the splash is up and the menu
# actually needs the visuals. Each item keeps its own instance: a WPF visual can
# only have one parent.
function Initialize-MenuItemIcons
{
param($Items)
foreach($item in @($Items))
{
if($null -eq $item) { continue }
if($null -ne $item.IconImage) { continue }
if(-not $item.PSObject.Methods['LoadIconImage']) { continue }
try { $item.LoadIconImage() } catch { }
}
}
+675
View File
@@ -0,0 +1,675 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Color resources -->
<SolidColorBrush x:Key="TitleBackgroundColor" Color="#FF1E1E1E"/>
<SolidColorBrush x:Key="TitleForegroundColor" Color="#FFCCCCCC"/>
<!-- Settings-dialog section headers only. Deliberately NOT TitleBackgroundColor:
that brush doubles as the window/modal title-bar background (#FF1E1E1E =
WindowBackgroundColor), which made section headers invisible in Dark. -->
<SolidColorBrush x:Key="SettingsSectionBackgroundColor" Color="#FF3F3F3F"/>
<SolidColorBrush x:Key="TextColor" Color="#FFEEEEEE"/>
<SolidColorBrush x:Key="SelectedRowBackgroundColor" Color="#FF555555" />
<SolidColorBrush x:Key="MismatchColor" Color="#FFFF7B72"/>
<!-- Selection highlight, as the SystemColors keys rather than as a settable
property: DataGridCell, TreeViewItem and ListBoxItem all bind their selected
state to these in their default templates, so a theme that does not override
them paints the OS accent no matter what the row style says. These are the
values that used to be hardcoded inside each DataGridRow style; they live
here now so Light can carry its own. Keep them in step with
SelectedRowBackgroundColor and TextColor above. -->
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#FF555555"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#FFEEEEEE"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#FF555555"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="#FFEEEEEE"/>
<SolidColorBrush x:Key="WindowBackgroundColor" Color="#FF1E1E1E"/>
<SolidColorBrush x:Key="PanelBackgroundColor" Color="#FF252526"/>
<SolidColorBrush x:Key="ControlBackgroundColor" Color="#FF2D2D2D"/>
<SolidColorBrush x:Key="BorderColor" Color="#FF555555"/>
<!-- Foreground color for disabled controls. Dim enough to read as "muted /
non-interactive" against the dark background. -->
<SolidColorBrush x:Key="DisabledForegroundColor" Color="#FF777777"/>
<SolidColorBrush x:Key="PersonPictureForegroundThemeBrush" Color="#FF888888"/>
<SolidColorBrush x:Key="PersonPictureEllipseBadgeForegroundThemeBrush" Color="#FF888888"/>
<SolidColorBrush x:Key="PersonPictureEllipseBadgeFillThemeBrush" Color="#FF888888"/>
<SolidColorBrush x:Key="PersonPictureEllipseBadgeStrokeThemeBrush" Color="#FF888888"/>
<SolidColorBrush x:Key="PersonPictureEllipseFillThemeBrush" Color="#FF888888"/>
<x:Double x:Key="PersonPictureEllipseBadgeStrokeOpacity">0.8</x:Double>
<x:Double x:Key="PersonPictureEllipseBadgeImageSourceStrokeOpacity">1.0</x:Double>
<x:Double x:Key="PersonPictureEllipseStrokeThickness">0</x:Double>
<x:Double x:Key="PersonPictureEllipseBadgeStrokeThickness">2</x:Double>
<!-- System color overrides for menus and hyperlinks -->
<Color x:Key="{x:Static SystemColors.HotTrackColorKey}" >#FF6CB3F5</Color>
<SolidColorBrush x:Key="{x:Static SystemColors.HotTrackBrushKey}" Color="#FF6CB3F5"/>
<SolidColorBrush x:Key="{x:Static SystemColors.MenuBrushKey}" Color="#FF2D2D2D"/>
<SolidColorBrush x:Key="{x:Static SystemColors.MenuBarBrushKey}" Color="#FF1E1E1E"/>
<SolidColorBrush x:Key="{x:Static SystemColors.MenuTextBrushKey}" Color="#FFEEEEEE"/>
<SolidColorBrush x:Key="{x:Static SystemColors.MenuHighlightBrushKey}" Color="#FF555555"/>
<!-- Default control styles -->
<Style TargetType="{x:Type Window}">
<Setter Property="Background" Value="{StaticResource WindowBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type Label}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="CaretBrush" Value="{StaticResource TextColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="2,1"/>
</Style>
<Style TargetType="{x:Type PasswordBox}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="CaretBrush" Value="{StaticResource TextColor}"/>
</Style>
<!-- TreeView / TreeViewItem: WPF ships no themed default for these, so a
bare TreeView rendered with the OS colours - a white panel inside the
dark window (the ADMX Import category tree). Item selection uses the
SystemColors keys because the default TreeViewItem template binds the
highlight to them rather than to a settable property. -->
<Style TargetType="{x:Type TreeView}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style TargetType="{x:Type TreeViewItem}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#FF555555"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#FFEEEEEE"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#FF555555"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="#FFEEEEEE"/>
</Style.Resources>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border x:Name="border" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource SelectedRowBackgroundColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="#FF777777"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid x:Name="root">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Border x:Name="mainBorder"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"/>
<ContentPresenter Grid.Column="0"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding SelectionBoxItem}"
ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
VerticalAlignment="Center"
IsHitTestVisible="False"/>
<Path Grid.Column="1"
Data="M0,0 L4,4 L8,0"
Stroke="{StaticResource TextColor}"
StrokeThickness="1.5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False"/>
<ToggleButton Grid.ColumnSpan="2"
Focusable="False"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Background="Transparent"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
</ToggleButton>
<Popup x:Name="PART_Popup"
Grid.ColumnSpan="2"
IsOpen="{TemplateBinding IsDropDownOpen}"
Placement="Bottom"
AllowsTransparency="True"
Focusable="False">
<Border Background="{StaticResource ControlBackgroundColor}"
BorderBrush="{StaticResource BorderColor}"
BorderThickness="1"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
MinWidth="{Binding ActualWidth, ElementName=root}">
<ScrollViewer>
<ItemsPresenter/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="mainBorder" Property="BorderBrush" Value="{StaticResource TextColor}"/>
</Trigger>
<Trigger Property="IsDropDownOpen" Value="True">
<Setter TargetName="mainBorder" Property="BorderBrush" Value="{StaticResource TextColor}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type CheckBox}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<BulletDecorator Background="Transparent">
<BulletDecorator.Bullet>
<Border x:Name="checkBorder"
Width="13" Height="13"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1">
<Path x:Name="checkMark"
Data="M 1,5 L 4,8 L 8,1"
Stroke="{StaticResource TextColor}"
StrokeThickness="1.5"
Visibility="Collapsed"
SnapsToDevicePixels="False"/>
</Border>
</BulletDecorator.Bullet>
<ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" RecognizesAccessKey="True"/>
</BulletDecorator>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="checkMark" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter TargetName="checkMark" Property="Visibility" Value="Visible"/>
<Setter TargetName="checkMark" Property="Opacity" Value="0.5"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="checkBorder" Property="BorderBrush" Value="{StaticResource TextColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="checkBorder" Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type RadioButton}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RadioButton}">
<BulletDecorator Background="Transparent">
<BulletDecorator.Bullet>
<Border x:Name="outerBorder"
Width="13" Height="13"
CornerRadius="6.5"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1">
<Ellipse x:Name="checkMark"
Width="5" Height="5"
Fill="{StaticResource TextColor}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="Collapsed"/>
</Border>
</BulletDecorator.Bullet>
<ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" RecognizesAccessKey="True"/>
</BulletDecorator>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="checkMark" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="outerBorder" Property="BorderBrush" Value="{StaticResource TextColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="outerBorder" Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="5,2"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#FF555555"/>
<Setter Property="Foreground" Value="#FFEEEEEE"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#FF666666"/>
<Setter Property="Foreground" Value="#FFEEEEEE"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type ListView}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="Background" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type Menu}">
<Setter Property="Background" Value="{StaticResource WindowBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="Padding" Value="6,3"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}">
<Grid>
<Border x:Name="border"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<Grid>
<Grid.ColumnDefinitions>
<!-- MinWidth=18 reserves a consistent left gutter for
icons + check marks so all rows in a context menu
line up even when only some carry a tick. -->
<ColumnDefinition Width="Auto" MinWidth="18"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="0,0,6,0">
<ContentPresenter x:Name="iconPresenter"
ContentSource="Icon"
VerticalAlignment="Center"/>
<Path x:Name="checkMark"
Data="M 0 4 L 3 7 L 8 1"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1.5"
Width="10" Height="8"
VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="Collapsed"/>
</Grid>
<ContentPresenter Grid.Column="1"
ContentSource="Header"
RecognizesAccessKey="True"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="2"
Text="{TemplateBinding InputGestureText}"
VerticalAlignment="Center"
Margin="12,0,0,0"
Opacity="0.7"/>
<!-- Arrow column auto-sizes to 0 when arrow Visibility
is Collapsed (the default — only Visible on
SubmenuHeader items). No right gutter on plain items. -->
<Path x:Name="arrow"
Grid.Column="3"
Data="M 0 0 L 4 3.5 L 0 7 Z"
Fill="{StaticResource TextColor}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="8,0,0,0"
Visibility="Collapsed"/>
</Grid>
</Border>
<Popup x:Name="PART_Popup"
AllowsTransparency="True"
Focusable="False"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Bottom"
PopupAnimation="Fade">
<Border Background="{StaticResource ControlBackgroundColor}"
BorderBrush="{StaticResource BorderColor}"
BorderThickness="1"
Padding="0,2">
<ItemsPresenter/>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Role" Value="TopLevelHeader">
<Setter Property="Padding" Value="8,2"/>
</Trigger>
<Trigger Property="Role" Value="TopLevelItem">
<Setter Property="Padding" Value="8,2"/>
</Trigger>
<Trigger Property="Role" Value="SubmenuHeader">
<Setter TargetName="arrow" Property="Visibility" Value="Visible"/>
<Setter TargetName="PART_Popup" Property="Placement" Value="Right"/>
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource SelectedRowBackgroundColor}"/>
</Trigger>
<Trigger Property="IsSubmenuOpen" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource SelectedRowBackgroundColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource DisabledForegroundColor}"/>
</Trigger>
<Trigger Property="Icon" Value="{x:Null}">
<Setter TargetName="iconPresenter" Property="Visibility" Value="Collapsed"/>
</Trigger>
<!-- Show the tick whenever the item is checked; force the icon
presenter Collapsed in the same trigger so an item with both
an Icon and IsChecked doesn't render them on top of each
other. -->
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="checkMark" Property="Visibility" Value="Visible"/>
<Setter TargetName="iconPresenter" Property="Visibility" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type Separator}">
<Setter Property="Background" Value="{StaticResource BorderColor}"/>
<Setter Property="Margin" Value="0,2"/>
<Setter Property="Height" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Separator}">
<Rectangle Height="1" Fill="{TemplateBinding Background}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="{x:Type Separator}">
<Setter Property="Background" Value="{StaticResource BorderColor}"/>
<Setter Property="Margin" Value="6,2"/>
<Setter Property="Height" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Separator}">
<Rectangle Height="1" Fill="{TemplateBinding Background}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ContextMenu}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<!-- Replace the default Aero2 template, which bakes in a hardcoded
#F1F1F1 icon-gutter Rectangle on the left and renders as a white
stripe under dark theme. -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContextMenu}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
Padding="0,2">
<ItemsPresenter Grid.IsSharedSizeScope="True"
KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TabControl}">
<Setter Property="Background" Value="{StaticResource WindowBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Background" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="Padding" Value="8,3"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1,1,1,0"
Padding="{TemplateBinding Padding}"
Margin="0,2,2,0">
<ContentPresenter ContentSource="Header"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="border" Property="Background" Value="#FF3C3C3C"/>
<Setter TargetName="border" Property="Margin" Value="0,0,2,-1"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="border" Property="Background" Value="#FF3C3C3C"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type DataGrid}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="RowBackground" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="AlternatingRowBackground" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="HorizontalGridLinesBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="VerticalGridLinesBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="RowHeaderWidth" Value="0"/>
</Style>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="SeparatorBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="Transparent"/>
</Style>
<Style TargetType="{x:Type ScrollViewer}">
<Setter Property="Background" Value="{StaticResource WindowBackgroundColor}"/>
</Style>
<Style TargetType="{x:Type ScrollBar}">
<Setter Property="Background" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="Width" Value="14"/>
<Setter Property="MinWidth" Value="14"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollBar}">
<Border Background="{TemplateBinding Background}">
<Track x:Name="PART_Track" IsDirectionReversed="True">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageUpCommand">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Border x:Name="tb" Background="#FF555555" CornerRadius="3" Margin="2,0"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="tb" Property="Background" Value="#FF666666"/>
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="tb" Property="Background" Value="#FF777777"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageDownCommand">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="Height" Value="14"/>
<Setter Property="MinHeight" Value="14"/>
<Setter Property="Width" Value="Auto"/>
<Setter Property="MinWidth" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollBar}">
<Border Background="{TemplateBinding Background}">
<Track x:Name="PART_Track" IsDirectionReversed="False">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageLeftCommand">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Border x:Name="tb" Background="#FF555555" CornerRadius="3" Margin="0,2"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="tb" Property="Background" Value="#FF666666"/>
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="tb" Property="Background" Value="#FF777777"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageRightCommand">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type GroupBox}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type ToolTip}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type Hyperlink}">
<Setter Property="Foreground" Value="#FF6CB3F5"/>
<Setter Property="TextDecorations" Value="None"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="#FF9ECBFF"/>
<Setter Property="TextDecorations" Value="Underline"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
+689
View File
@@ -0,0 +1,689 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--
Light palette, refreshed 2026-08-23 to sit closer to Windows 11.
Keep these values in step with the Light dictionary in
UI/Avalonia/Themes/Brushes.axaml - the two backends are meant to look the
same, and the Avalonia file carries the same comments.
The headline change is TitleBackgroundColor: it was #FF9B9B9B, a heavy
mid-grey that read as a Windows 7 toolbar. It cannot simply go white,
though - it backs the title strips (MainWindow caption, the Intune
Manager panel header, EditScriptDialog) which must stay distinct from the
window, so it becomes a light grey rather than disappearing.
-->
<!-- Title strips. Dark text sits on this, so it stays well above mid-grey. -->
<SolidColorBrush x:Key="TitleBackgroundColor" Color="#FFECECEC"/>
<!-- Settings-dialog section headers. One step darker than the title strip so a
header still reads as a header when the two are adjacent; previously both
were #FF9B9B9B, which stopped working once the title strip lightened. -->
<SolidColorBrush x:Key="SettingsSectionBackgroundColor" Color="#FFE0E0E0"/>
<SolidColorBrush x:Key="TitleForegroundColor" Color="#FF1A1A1A"/>
<!-- Near-black rather than pure black: matches the Windows 11 text colour and
is noticeably less harsh on a white background. -->
<SolidColorBrush x:Key="TextColor" Color="#FF1A1A1A"/>
<!-- Selection reads as selected rather than merely grey. This is the long-standing
Explorer selection tint, so it stays recognisable regardless of accent colour. -->
<SolidColorBrush x:Key="SelectedRowBackgroundColor" Color="#FFE5F1FB" />
<SolidColorBrush x:Key="MismatchColor" Color="#FFC42B1C"/>
<!-- Selection highlight, as the SystemColors keys rather than as a settable
property: DataGridCell, TreeViewItem and ListBoxItem all bind their selected
state to these in their default templates, so a theme that does not override
them paints the OS accent no matter what the row style says.
They belong to the THEME because their whole job is to carry the theme's
selection colours. They used to be redefined inside each DataGridRow style
(Themes/Styles.xaml plus three window XAML files), hardcoded to the DARK
palette, which is how a selected row in Light rendered as a dark grey band
with dark text on it - the cell painted #FF555555 while the row's own trigger
set the foreground to this theme's near-black TextColor. -->
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#FFE5F1FB"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#FF1A1A1A"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#FFE5F1FB"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="#FF1A1A1A"/>
<SolidColorBrush x:Key="WindowBackgroundColor" Color="#FFFFFFFF"/>
<!-- Panels are a light layer over the white window, not a distinct grey slab. -->
<SolidColorBrush x:Key="PanelBackgroundColor" Color="#FFF3F3F3"/>
<SolidColorBrush x:Key="ControlBackgroundColor" Color="#FFFFFFFF"/>
<!-- Hairline separators; #FFCCCCCC was heavy enough to read as a drawn box. -->
<SolidColorBrush x:Key="BorderColor" Color="#FFE1E1E1"/>
<!-- Foreground color for disabled controls. Light enough to stay visible against
the white window background while reading as "muted / non-interactive". -->
<SolidColorBrush x:Key="DisabledForegroundColor" Color="#FF9A9A9A"/>
<SolidColorBrush x:Key="PersonPictureForegroundThemeBrush" Color="#cccccc"/>
<SolidColorBrush x:Key="PersonPictureEllipseBadgeForegroundThemeBrush" Color="#cccccc"/>
<SolidColorBrush x:Key="PersonPictureEllipseBadgeFillThemeBrush" Color="#cccccc"/>
<SolidColorBrush x:Key="PersonPictureEllipseBadgeStrokeThemeBrush" Color="#cccccc"/>
<SolidColorBrush x:Key="PersonPictureEllipseFillThemeBrush" Color="#cccccc"/>
<x:Double x:Key="PersonPictureEllipseBadgeStrokeOpacity">0.8</x:Double>
<x:Double x:Key="PersonPictureEllipseBadgeImageSourceStrokeOpacity">1.0</x:Double>
<x:Double x:Key="PersonPictureEllipseStrokeThickness">0</x:Double>
<x:Double x:Key="PersonPictureEllipseBadgeStrokeThickness">2</x:Double>
<!-- Default control styles -->
<Style TargetType="{x:Type Window}">
<Setter Property="Background" Value="{StaticResource WindowBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type Label}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="CaretBrush" Value="{StaticResource TextColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="2,1"/>
</Style>
<Style TargetType="{x:Type PasswordBox}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="CaretBrush" Value="{StaticResource TextColor}"/>
</Style>
<!-- TreeView / TreeViewItem: WPF ships no themed default for these, so a
bare TreeView rendered with the OS colours - a white panel inside the
dark window (the ADMX Import category tree). Item selection uses the
SystemColors keys because the default TreeViewItem template binds the
highlight to them rather than to a settable property. -->
<Style TargetType="{x:Type TreeView}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style TargetType="{x:Type TreeViewItem}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#FFE5F1FB"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#FF1A1A1A"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#FFE5F1FB"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="#FF1A1A1A"/>
</Style.Resources>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border x:Name="border" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource SelectedRowBackgroundColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="#FF888888"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid x:Name="root">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Border x:Name="mainBorder"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"/>
<ContentPresenter Grid.Column="0"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding SelectionBoxItem}"
ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
VerticalAlignment="Center"
IsHitTestVisible="False"/>
<Path Grid.Column="1"
Data="M0,0 L4,4 L8,0"
Stroke="{StaticResource TextColor}"
StrokeThickness="1.5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False"/>
<ToggleButton Grid.ColumnSpan="2"
Focusable="False"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Background="Transparent"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
</ToggleButton>
<Popup x:Name="PART_Popup"
Grid.ColumnSpan="2"
IsOpen="{TemplateBinding IsDropDownOpen}"
Placement="Bottom"
AllowsTransparency="True"
Focusable="False">
<Border Background="{StaticResource ControlBackgroundColor}"
BorderBrush="{StaticResource BorderColor}"
BorderThickness="1"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
MinWidth="{Binding ActualWidth, ElementName=root}">
<ScrollViewer>
<ItemsPresenter/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="mainBorder" Property="BorderBrush" Value="{StaticResource TextColor}"/>
</Trigger>
<Trigger Property="IsDropDownOpen" Value="True">
<Setter TargetName="mainBorder" Property="BorderBrush" Value="{StaticResource TextColor}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type CheckBox}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<BulletDecorator Background="Transparent">
<BulletDecorator.Bullet>
<Border x:Name="checkBorder"
Width="13" Height="13"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1">
<Path x:Name="checkMark"
Data="M 1,5 L 4,8 L 8,1"
Stroke="{StaticResource TextColor}"
StrokeThickness="1.5"
Visibility="Collapsed"
SnapsToDevicePixels="False"/>
</Border>
</BulletDecorator.Bullet>
<ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" RecognizesAccessKey="True"/>
</BulletDecorator>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="checkMark" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter TargetName="checkMark" Property="Visibility" Value="Visible"/>
<Setter TargetName="checkMark" Property="Opacity" Value="0.5"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="checkBorder" Property="BorderBrush" Value="{StaticResource TextColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="checkBorder" Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type RadioButton}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RadioButton}">
<BulletDecorator Background="Transparent">
<BulletDecorator.Bullet>
<Border x:Name="outerBorder"
Width="13" Height="13"
CornerRadius="6.5"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1">
<Ellipse x:Name="checkMark"
Width="5" Height="5"
Fill="{StaticResource TextColor}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="Collapsed"/>
</Border>
</BulletDecorator.Bullet>
<ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" RecognizesAccessKey="True"/>
</BulletDecorator>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="checkMark" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="outerBorder" Property="BorderBrush" Value="{StaticResource TextColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="outerBorder" Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="5,2"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#FFD0D0D0"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#FFBEBEBE"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type ListView}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="Background" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type Menu}">
<Setter Property="Background" Value="{StaticResource WindowBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
</Style>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="Padding" Value="6,3"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}">
<Grid>
<Border x:Name="border"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<Grid>
<Grid.ColumnDefinitions>
<!-- MinWidth=18 reserves a consistent left gutter for
icons + check marks so all rows in a context menu
line up even when only some carry a tick. -->
<ColumnDefinition Width="Auto" MinWidth="18"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="0,0,6,0">
<ContentPresenter x:Name="iconPresenter"
ContentSource="Icon"
VerticalAlignment="Center"/>
<!-- Tick glyph for IsCheckable items. Stroke matches
the row Foreground so it picks up disabled colour
automatically. -->
<Path x:Name="checkMark"
Data="M 0 4 L 3 7 L 8 1"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1.5"
Width="10" Height="8"
VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="Collapsed"/>
</Grid>
<ContentPresenter Grid.Column="1"
ContentSource="Header"
RecognizesAccessKey="True"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="2"
Text="{TemplateBinding InputGestureText}"
VerticalAlignment="Center"
Margin="12,0,0,0"
Opacity="0.7"/>
<Path x:Name="arrow"
Grid.Column="3"
Data="M 0 0 L 4 3.5 L 0 7 Z"
Fill="{StaticResource TextColor}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="8,0,0,0"
Visibility="Collapsed"/>
</Grid>
</Border>
<Popup x:Name="PART_Popup"
AllowsTransparency="True"
Focusable="False"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Bottom"
PopupAnimation="Fade">
<Border Background="{StaticResource ControlBackgroundColor}"
BorderBrush="{StaticResource BorderColor}"
BorderThickness="1"
Padding="0,2">
<ItemsPresenter/>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Role" Value="TopLevelHeader">
<Setter Property="Padding" Value="8,2"/>
</Trigger>
<Trigger Property="Role" Value="TopLevelItem">
<Setter Property="Padding" Value="8,2"/>
</Trigger>
<Trigger Property="Role" Value="SubmenuHeader">
<Setter TargetName="arrow" Property="Visibility" Value="Visible"/>
<Setter TargetName="PART_Popup" Property="Placement" Value="Right"/>
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource SelectedRowBackgroundColor}"/>
</Trigger>
<Trigger Property="IsSubmenuOpen" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource SelectedRowBackgroundColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource DisabledForegroundColor}"/>
</Trigger>
<Trigger Property="Icon" Value="{x:Null}">
<Setter TargetName="iconPresenter" Property="Visibility" Value="Collapsed"/>
</Trigger>
<!-- Show the tick whenever the item is checked. We also force
the icon presenter Collapsed in the same trigger so an item
with both an Icon and IsChecked doesn't render them on top
of each other. -->
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="checkMark" Property="Visibility" Value="Visible"/>
<Setter TargetName="iconPresenter" Property="Visibility" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type Separator}">
<Setter Property="Background" Value="{StaticResource BorderColor}"/>
<Setter Property="Margin" Value="0,2"/>
<Setter Property="Height" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Separator}">
<Rectangle Height="1" Fill="{TemplateBinding Background}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="{x:Type Separator}">
<Setter Property="Background" Value="{StaticResource BorderColor}"/>
<Setter Property="Margin" Value="6,2"/>
<Setter Property="Height" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Separator}">
<Rectangle Height="1" Fill="{TemplateBinding Background}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ContextMenu}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<!-- Replace the default Aero2 template to drop the hardcoded
#F1F1F1 icon-gutter Rectangle on the left, matching Dark.xaml so
both themes render context menus consistently. -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContextMenu}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
Padding="0,2">
<ItemsPresenter Grid.IsSharedSizeScope="True"
KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TabControl}">
<Setter Property="Background" Value="{StaticResource WindowBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Background" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="Padding" Value="8,3"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1,1,1,0"
Padding="{TemplateBinding Padding}"
Margin="0,2,2,0">
<ContentPresenter ContentSource="Header"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter TargetName="border" Property="Margin" Value="0,0,2,-1"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="border" Property="Background" Value="#FFD0D0D0"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type DataGrid}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="RowBackground" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="AlternatingRowBackground" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="HorizontalGridLinesBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="VerticalGridLinesBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="RowHeaderWidth" Value="0"/>
</Style>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
<Setter Property="SeparatorBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="Transparent"/>
</Style>
<Style TargetType="{x:Type ScrollViewer}">
<Setter Property="Background" Value="{StaticResource WindowBackgroundColor}"/>
</Style>
<Style TargetType="{x:Type ScrollBar}">
<Setter Property="Background" Value="{StaticResource PanelBackgroundColor}"/>
<Setter Property="Width" Value="14"/>
<Setter Property="MinWidth" Value="14"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollBar}">
<Border Background="{TemplateBinding Background}">
<Track x:Name="PART_Track" IsDirectionReversed="True">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageUpCommand">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Border x:Name="tb" Background="#FFBBBBBB" CornerRadius="3" Margin="2,0"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="tb" Property="Background" Value="#FF999999"/>
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="tb" Property="Background" Value="#FF888888"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageDownCommand">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="Height" Value="14"/>
<Setter Property="MinHeight" Value="14"/>
<Setter Property="Width" Value="Auto"/>
<Setter Property="MinWidth" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollBar}">
<Border Background="{TemplateBinding Background}">
<Track x:Name="PART_Track" IsDirectionReversed="False">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageLeftCommand">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Border x:Name="tb" Background="#FFBBBBBB" CornerRadius="3" Margin="0,2"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="tb" Property="Background" Value="#FF999999"/>
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="tb" Property="Background" Value="#FF888888"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageRightCommand">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type GroupBox}">
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type ToolTip}">
<Setter Property="Background" Value="{StaticResource ControlBackgroundColor}"/>
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
</Style>
<Style TargetType="{x:Type Hyperlink}">
<Setter Property="Foreground" Value="#FF0066CC"/>
<Setter Property="TextDecorations" Value="None"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="#FF004499"/>
<Setter Property="TextDecorations" Value="Underline"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
+179
View File
@@ -0,0 +1,179 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="InfoIcon" TargetType="{x:Type Rectangle}">
<Setter Property="Width" Value="12"/>
<Setter Property="Height" Value="12"/>
<Setter Property="Fill">
<Setter.Value>
<DrawingBrush>
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M4,7.5C4,5.015 6.015,3 8.5,3 10.985,3 13,5.015 13,7.5 13,9.985 10.985,12 8.5,12 6.015,12 4,9.985 4,7.5" />
<GeometryDrawing Brush="#FF414141" Geometry="F1M9,6L8,6 8,5 9,5z M9,10L8,10 8,7 9,7z M8.5,4C6.567,4 5,5.567 5,7.5 5,9.433 6.567,11 8.5,11 10.433,11 12,9.433 12,7.5 12,5.567 10.433,4 8.5,4" />
<GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M8,6L9,6 9,5 8,5z M8,10L9,10 9,7 8,7z" />
</DrawingGroup.Children>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="HoverUnderlineStyle" TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="TextBlock.TextDecorations" Value="Underline" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="HoverUnderlineStyleWithBackground" TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="TextBlock.TextDecorations" Value="Underline" />
<Setter Property="TextBlock.Background" Value="LightGray" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key='ErrorControl' TargetType='ContentControl'>
<Setter Property='Template'>
<Setter.Value>
<ControlTemplate TargetType='ContentControl'>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Rectangle Width="16" Height="16" ToolTip="{Binding ToolTip}" Name="errorIcon" Visibility="Collapsed">
<Rectangle.Fill>
<DrawingBrush>
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M8,16C3.589,16 0,12.411 0,8 0,3.589 3.589,0 8,0 12.411,0 16,3.589 16,8 16,12.411 12.411,16 8,16" />
<GeometryDrawing Brush="#FFE41400" Geometry="F1M9,10L7,10 7,3 9,3z M9,13L7,13 7,11 9,11z M8,1C4.134,1 1,4.134 1,8 1,11.865 4.134,15 8,15 11.865,15 15,11.865 15,8 15,4.134 11.865,1 8,1" />
<GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M9,11L7,11 7,13 9,13z M9,10L7,10 7,3 9,3z" />
</DrawingGroup.Children>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
<ContentPresenter Grid.Column="1" Name="errorControl" />
</Grid>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource self},Path=Tag}" Value="True">
<Setter TargetName="errorIcon" Property="Visibility" Value="Visible" />
</DataTrigger>
</ControlTemplate.Triggers>
<ControlTemplate.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag, RelativeSource={RelativeSource AncestorType={x:Type ContentControl}}}" Value="True">
<Setter Property="Border.BorderBrush" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</ControlTemplate.Resources>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ErrorBorder" TargetType="Control">
<Style.Triggers>
<DataTrigger Binding="{Binding HasErrors}" Value="True">
<Setter Property="Border.BorderBrush" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="LinkButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<TextBlock Style="{DynamicResource HoverUnderlineStyle}" HorizontalAlignment="Left">
<ContentPresenter />
</TextBlock>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="{DynamicResource TextColor}" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" Value="{DynamicResource SelectedRowBackgroundColor}" />
</Trigger>
<!-- Visually gray-out when the button is disabled; the default
IsEnabled=False handling on Button doesn't propagate to the
templated TextBlock, so the text color has to be set explicitly.
Uses the per-theme DisabledForegroundColor so it reads correctly
in both Default (light) and Dark themes. -->
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource DisabledForegroundColor}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Cursor" Value="Arrow" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="ContentButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="{DynamicResource TextColor}" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" Value="{DynamicResource SelectedRowBackgroundColor}" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="{DynamicResource TextColor}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Name="Border" Padding="2" SnapsToDevicePixels="true">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SelectedRowBackgroundColor}"/>
</Trigger>
<Trigger Property="UIElement.IsMouseOver" Value="True">
<Setter Property="Panel.Background" TargetName="Border" Value="{DynamicResource SelectedRowBackgroundColor}"/>
<Setter Property="Border.BorderBrush" TargetName="Border" Value="{DynamicResource SelectedRowBackgroundColor}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- The selection highlight brushes this style used to redefine now live in the
theme dictionaries (Themes/Light.xaml, Themes/Dark.xaml). They were hardcoded
to the dark palette here, so a selected row in the Light theme painted a dark
grey cell background under this style's near-black Foreground. This file is
shared by both themes and cannot hold a colour. -->
<Style TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SelectedRowBackgroundColor}" />
<Setter Property="Foreground" Value="{DynamicResource TextColor}" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource SelectedRowBackgroundColor}" />
<Setter Property="Foreground" Value="{DynamicResource TextColor}" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
+59
View File
@@ -0,0 +1,59 @@
<!-- <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="About" SizeToContent="Height" WindowStartupLocation="CenterScreen" ResizeMode="NoResize" Width="300"> -->
<Border xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="{DynamicResource WindowBackgroundColor}"
TextElement.Foreground="{DynamicResource TextColor}">
<Grid Name="grdAbout">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Height="20" Name="txtTitle" FontWeight="Bold" Margin="0,5,0,0" />
<TextBlock Grid.Row="1" Text="(c) 2022 Mikael Karlsson - MIT License" Margin="0,5,0,0" />
<TextBlock Grid.Row="2">
See
<Hyperlink Name="linkSource" NavigateUri="https://github.com/Micke-K/IntuneManagement">
GitHub
</Hyperlink> for more information.
<LineBreak/>
Like the tool?
<Hyperlink Name="linkCoffee" NavigateUri="https://buymeacoffee.com/MickeK">
Buy me a coffee
</Hyperlink>
</TextBlock>
<TextBlock Height="20" Name="txtViewTitle" Grid.Column="1" Margin="10,5,0,0" />
<TextBlock Name="txtViewDescription" Grid.Row="1" Grid.Column="1" Grid.RowSpan="3" VerticalAlignment="Top" Margin="10,5,0,0" TextWrapping="Wrap" />
<TextBlock Grid.Row="3" Text="Loaded modules:" Margin="0,5,0,0" />
<ListBox Name="lstModules" SelectionMode="Single" Grid.Row="4" Grid.ColumnSpan="2" Grid.IsSharedSizeScope='True'>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="NameColumn" />
<ColumnDefinition Width="Auto" SharedSizeGroup="VersionColumn" />
<ColumnDefinition Width="Auto" SharedSizeGroup="TypeColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" Grid.Column='0' Margin="0,0,0,0" />
<TextBlock Text="{Binding Version}" Grid.Column='1' Margin="10,0,0,0" />
<TextBlock Text="{Binding Type}" Grid.Column='2' Margin="10,0,0,0" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
+126
View File
@@ -0,0 +1,126 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,5,0">
<Label Content="Action" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Add: union of current and selected. Replace: overwrite all current assignments. Remove: subtract selected from current." />
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5,0,5">
<RadioButton Name="rbBulkAssignAdd" Content="Add" GroupName="BulkAssignAction" IsChecked="True" VerticalAlignment="Center" />
<RadioButton Name="rbBulkAssignReplace" Content="Replace" GroupName="BulkAssignAction" Margin="15,0,0,0" VerticalAlignment="Center" />
<RadioButton Name="rbBulkAssignRemove" Content="Remove" GroupName="BulkAssignAction" Margin="15,0,0,0" VerticalAlignment="Center" />
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="1" Margin="0,0,5,0">
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Regex (or literal substring) matched against each policy's Name. Leave empty to target every listed policy." />
</StackPanel>
<TextBox Grid.Column="1" Grid.Row="1" Name="txtBulkAssignNameFilter" Margin="0,5,0,5"
Text="{Binding Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel Orientation="Horizontal" Grid.Row="2" Margin="0,0,5,0">
<Label Content="List by" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Group: pick whole categories (Configuration, Compliance, ...). API: pick individual policy types. Apps and Health Scripts are supported — configure per-policy fields via the App settings... button." />
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5,0,5">
<RadioButton Name="rbBulkAssignViewGroup" Content="Group" GroupName="BulkAssignView" IsChecked="True" VerticalAlignment="Center" />
<RadioButton Name="rbBulkAssignViewType" Content="API" GroupName="BulkAssignView" Margin="15,0,0,0" VerticalAlignment="Center" />
</StackPanel>
</Grid>
<Grid Grid.Row="1" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,3">
<Label Content="Objects to update" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select the policy types / groups to update" />
</StackPanel>
<DataGrid Grid.Row="1" Name="dgBulkAssignObjects" CanUserAddRows="False" AutoGenerateColumns="False"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Background="{DynamicResource ControlBackgroundColor}" />
</Grid>
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,3">
<Label Content="Assignments to apply" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Targets that will be added / replaced / removed. Double-click a row to open settings." />
</StackPanel>
<DataGrid Grid.Row="1" Name="dgBulkAssignList" CanUserAddRows="False" AutoGenerateColumns="False"
SelectionMode="Single" SelectionUnit="FullRow"
Background="{DynamicResource ControlBackgroundColor}">
<DataGrid.Columns>
<DataGridTextColumn Header="Target type" Binding="{Binding TargetTypeDisplay}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Group" Binding="{Binding GroupName}" IsReadOnly="True" Width="2*" />
<!-- Intent only applies when the policy being updated is an
app (mobileAppAssignments shape). Shown as a display
column here; the Intent picker in the Add dialog
drives the value, so this is read-only. -->
<DataGridTextColumn Header="Intent" Binding="{Binding Intent}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Filter" Binding="{Binding FilterDisplay}" IsReadOnly="True" Width="1*" />
<!-- "Settings" shows the count of per-platform settings tabs
currently configured for this row. 0 = Graph defaults.
Configure via the "App settings..." button below. -->
<DataGridTextColumn Header="App settings" Binding="{Binding SettingsDisplay}" IsReadOnly="True" Width="Auto" />
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,5,0,0">
<Button Name="btnBulkAssignAddGroup" Content="Add group..." MinWidth="120" Margin="0,0,5,0"
ToolTip="Pick an AAD group to include or exclude" />
<Button Name="btnBulkAssignAddAllDev" Content="All devices" MinWidth="100" Margin="0,0,5,0"
ToolTip="Add the All Devices virtual target" />
<Button Name="btnBulkAssignAddAllUsers" Content="All users" MinWidth="100" Margin="0,0,5,0"
ToolTip="Add the All Users virtual target" />
<Button Name="btnBulkAssignSettings" Content="App settings..." MinWidth="120" Margin="0,0,5,0"
ToolTip="Configure per-platform app assignment settings for the selected row" />
<Button Name="btnBulkAssignRemoveSel" Content="Remove selected" MinWidth="120"
ToolTip="Remove the highlighted row from the list above" />
</StackPanel>
</Grid>
</Grid>
<Grid Grid.Row="2" Margin="0,5,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Name="txtBulkAssignStatus" VerticalAlignment="Center"
Foreground="{DynamicResource TextColor}" Opacity="0.8" />
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Name="btnBulkAssignApply" Content="Apply" MinWidth="100" Margin="5,0,0,0" />
<Button Name="btnBulkAssignClose" Content="Close" MinWidth="100" Margin="5,0,0,0" />
</StackPanel>
</Grid>
</Grid>
@@ -0,0 +1,54 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Margin="5,5,5,5" MinWidth="500" MinHeight="400" Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="PickerLabel" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Search:" VerticalAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="1" Name="txtGroupSearch" Margin="0,3,5,3" />
<Button Grid.Row="0" Grid.Column="2" Name="btnGroupSearch" Content="Search" MinWidth="80" />
<ListBox Grid.Row="1" Grid.ColumnSpan="3" Name="lstGroupResults" Margin="0,5,0,5"
SelectionMode="Single" DisplayMemberPath="displayName"
Background="{DynamicResource ControlBackgroundColor}" />
<Label Grid.Row="2" Grid.Column="0" Content="Direction:" VerticalAlignment="Center" />
<StackPanel Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5,0,5">
<RadioButton Name="rbGroupInclude" Content="Include" GroupName="GroupPickerDir" IsChecked="True" VerticalAlignment="Center" />
<RadioButton Name="rbGroupExclude" Content="Exclude" GroupName="GroupPickerDir" Margin="15,0,0,0" VerticalAlignment="Center" />
</StackPanel>
<Label Grid.Row="3" Grid.Column="0" Content="Filter:" VerticalAlignment="Center" />
<ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Name="cbGroupFilter" Margin="0,3,0,3"
DisplayMemberPath="Name" SelectedValuePath="Id" />
<Label Grid.Row="4" Grid.Column="0" Content="Filter mode:" VerticalAlignment="Center" />
<StackPanel Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5,0,5">
<RadioButton Name="rbGroupFilterInclude" Content="Include" GroupName="GroupPickerFilterDir" IsChecked="True" VerticalAlignment="Center" />
<RadioButton Name="rbGroupFilterExclude" Content="Exclude" GroupName="GroupPickerFilterDir" Margin="15,0,0,0" VerticalAlignment="Center" />
</StackPanel>
<!-- Intent only applies to app (mobileAppAssignments) targets. For non-
app types the public command ignores this value, so the picker still
shows it but with a tooltip explaining the scope. -->
<Label Grid.Row="5" Grid.Column="0" Content="App intent:" VerticalAlignment="Center" ToolTip="Only applies when the target policy is an app (mobile app assignment). Ignored for Device Configuration, Compliance, scripts, etc." />
<ComboBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Name="cbGroupIntent" Margin="0,3,0,3"
DisplayMemberPath="Name" SelectedValuePath="Value"
ToolTip="Required = auto-install. Available = visible in Company Portal. Uninstall = auto-remove. Available without enrollment = web/CP without MDM enrollment. Not available = hidden." />
<StackPanel Grid.Row="6" Grid.ColumnSpan="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0">
<Button Name="btnGroupPickerOK" Content="Add" MinWidth="100" Margin="0,0,5,0" />
<Button Name="btnGroupPickerCancel" Content="Cancel" MinWidth="100" />
</StackPanel>
</Grid>
+363
View File
@@ -0,0 +1,363 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Margin="5,5,5,5" MinWidth="640" MinHeight="500" Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Margin="0,0,0,6" TextWrapping="Wrap" Foreground="{DynamicResource TextColor}">
Per-platform app assignment settings. Tick "Apply these settings" on a tab to include it in
the POST for matching apps; leave it unticked to let Graph defaults stand.
</TextBlock>
<TabControl Grid.Row="1" Name="tcBulkAssignSettings" Background="{DynamicResource ControlBackgroundColor}">
<!-- ======================== iOS LOB ======================== -->
<TabItem Header="iOS LOB" Name="tabIosLob">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyIosLob" Content="Apply these settings to iOS LOB apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyIosLob, Path=IsChecked}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Is removable:" />
<CheckBox Grid.Row="0" Grid.Column="1" Name="chkIosLob_IsRemovable" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="1" Grid.Column="0" Content="Prevent backup:" />
<CheckBox Grid.Row="1" Grid.Column="1" Name="chkIosLob_PreventBackup" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="2" Grid.Column="0" Content="Uninstall on removal:" />
<CheckBox Grid.Row="2" Grid.Column="1" Name="chkIosLob_Uninstall" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="3" Grid.Column="0" Content="VPN config id:" />
<TextBox Grid.Row="3" Grid.Column="1" Name="txtIosLob_VpnId" Margin="0,5,0,5" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== iOS Store ======================== -->
<TabItem Header="iOS Store" Name="tabIosStore">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyIosStore" Content="Apply these settings to iOS Store apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyIosStore, Path=IsChecked}">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Is removable:" />
<CheckBox Grid.Row="0" Grid.Column="1" Name="chkIosStore_IsRemovable" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="1" Grid.Column="0" Content="Prevent backup:" />
<CheckBox Grid.Row="1" Grid.Column="1" Name="chkIosStore_PreventBackup" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="2" Grid.Column="0" Content="Uninstall on removal:" />
<CheckBox Grid.Row="2" Grid.Column="1" Name="chkIosStore_Uninstall" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="3" Grid.Column="0" Content="VPN config id:" />
<TextBox Grid.Row="3" Grid.Column="1" Name="txtIosStore_VpnId" Margin="0,5,0,5" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== iOS VPP ======================== -->
<TabItem Header="iOS VPP" Name="tabIosVpp">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyIosVpp" Content="Apply these settings to iOS VPP apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyIosVpp, Path=IsChecked}">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Use device licensing:" />
<CheckBox Grid.Row="0" Grid.Column="1" Name="chkIosVpp_DeviceLicensing" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="1" Grid.Column="0" Content="Is removable:" />
<CheckBox Grid.Row="1" Grid.Column="1" Name="chkIosVpp_IsRemovable" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="2" Grid.Column="0" Content="Prevent backup:" />
<CheckBox Grid.Row="2" Grid.Column="1" Name="chkIosVpp_PreventBackup" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="3" Grid.Column="0" Content="Prevent auto-update:" />
<CheckBox Grid.Row="3" Grid.Column="1" Name="chkIosVpp_PreventAutoUpdate" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="4" Grid.Column="0" Content="Uninstall on removal:" />
<CheckBox Grid.Row="4" Grid.Column="1" Name="chkIosVpp_Uninstall" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="5" Grid.Column="0" Content="VPN config id:" />
<TextBox Grid.Row="5" Grid.Column="1" Name="txtIosVpp_VpnId" Margin="0,5,0,5" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== iOS DDM LOB ======================== -->
<TabItem Header="iOS DDM" Name="tabIosDdm">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyIosDdm" Content="Apply these settings to iOS DDM LOB apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyIosDdm, Path=IsChecked}">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Associated domains:" />
<TextBox Grid.Row="0" Grid.Column="1" Name="txtIosDdm_Domains" Margin="0,5,0,5" Height="50" AcceptsReturn="True" TextWrapping="Wrap"
ToolTip="One domain per line." />
<Label Grid.Row="1" Grid.Column="0" Content="Direct download:" />
<CheckBox Grid.Row="1" Grid.Column="1" Name="chkIosDdm_DirectDownload" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="2" Grid.Column="0" Content="Prevent backup:" />
<CheckBox Grid.Row="2" Grid.Column="1" Name="chkIosDdm_PreventBackup" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="3" Grid.Column="0" Content="Tap-to-pay screen lock:" />
<CheckBox Grid.Row="3" Grid.Column="1" Name="chkIosDdm_TapToPay" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="4" Grid.Column="0" Content="VPN config id:" />
<TextBox Grid.Row="4" Grid.Column="1" Name="txtIosDdm_VpnId" Margin="0,5,0,5" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== Android Managed Store ======================== -->
<TabItem Header="Android Store" Name="tabAndroidStore">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyAndroidStore" Content="Apply these settings to Android Managed Store apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyAndroidStore, Path=IsChecked}">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Auto-update mode:" />
<ComboBox Grid.Row="0" Grid.Column="1" Name="cbAndroidStore_AutoUpdate" Margin="0,5,0,5" Width="200" HorizontalAlignment="Left" />
<Label Grid.Row="1" Grid.Column="0" Content="Track IDs:" />
<TextBox Grid.Row="1" Grid.Column="1" Name="txtAndroidStore_Tracks" Margin="0,5,0,5" Height="50" AcceptsReturn="True" TextWrapping="Wrap"
ToolTip="One track id per line. Leave empty for default production track." />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== macOS LOB ======================== -->
<TabItem Header="macOS LOB" Name="tabMacLob">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyMacLob" Content="Apply these settings to macOS LOB apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyMacLob, Path=IsChecked}">
<Grid.RowDefinitions><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Uninstall on removal:" />
<CheckBox Grid.Row="0" Grid.Column="1" Name="chkMacLob_Uninstall" VerticalAlignment="Center" Margin="0,5,0,5" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== macOS VPP ======================== -->
<TabItem Header="macOS VPP" Name="tabMacVpp">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyMacVpp" Content="Apply these settings to macOS VPP apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyMacVpp, Path=IsChecked}">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Use device licensing:" />
<CheckBox Grid.Row="0" Grid.Column="1" Name="chkMacVpp_DeviceLicensing" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="1" Grid.Column="0" Content="Prevent backup:" />
<CheckBox Grid.Row="1" Grid.Column="1" Name="chkMacVpp_PreventBackup" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="2" Grid.Column="0" Content="Prevent auto-update:" />
<CheckBox Grid.Row="2" Grid.Column="1" Name="chkMacVpp_PreventAutoUpdate" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="3" Grid.Column="0" Content="Uninstall on removal:" />
<CheckBox Grid.Row="3" Grid.Column="1" Name="chkMacVpp_Uninstall" VerticalAlignment="Center" Margin="0,5,0,5" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== Microsoft Store for Business ======================== -->
<TabItem Header="MS Store" Name="tabMsStore">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyMsStore" Content="Apply these settings to Microsoft Store for Business apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyMsStore, Path=IsChecked}">
<Grid.RowDefinitions><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Device context:" />
<CheckBox Grid.Row="0" Grid.Column="1" Name="chkMsStore_DeviceContext" VerticalAlignment="Center" Margin="0,5,0,5"
ToolTip="True = install in device context (all users on device). False = per-user." />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== Windows AppX ======================== -->
<TabItem Header="Windows AppX" Name="tabWinAppx">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyWinAppx" Content="Apply these settings to Windows AppX apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyWinAppx, Path=IsChecked}">
<Grid.RowDefinitions><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Device context:" />
<CheckBox Grid.Row="0" Grid.Column="1" Name="chkWinAppx_DeviceContext" VerticalAlignment="Center" Margin="0,5,0,5" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== Windows Universal AppX ======================== -->
<TabItem Header="Windows UAP" Name="tabWinUap">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyWinUap" Content="Apply these settings to Windows Universal AppX apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyWinUap, Path=IsChecked}">
<Grid.RowDefinitions><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Device context:" />
<CheckBox Grid.Row="0" Grid.Column="1" Name="chkWinUap_DeviceContext" VerticalAlignment="Center" Margin="0,5,0,5" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== Win32 LOB (also covers Win32 Catalog) ======================== -->
<TabItem Header="Win32 LOB" Name="tabWin32">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyWin32" Content="Apply these settings to Win32 LOB / Win32 Catalog apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyWin32, Path=IsChecked}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Delivery optimization:" />
<ComboBox Grid.Row="0" Grid.Column="1" Name="cbWin32_DeliveryOpt" Margin="0,5,0,5" Width="200" HorizontalAlignment="Left" />
<Label Grid.Row="1" Grid.Column="0" Content="Notifications:" />
<ComboBox Grid.Row="1" Grid.Column="1" Name="cbWin32_Notifications" Margin="0,5,0,5" Width="200" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.ColumnSpan="2" Text="Auto-update settings" FontWeight="SemiBold" Margin="0,8,0,4" />
<Label Grid.Row="3" Grid.Column="0" Content="Superseded apps:" />
<ComboBox Grid.Row="3" Grid.Column="1" Name="cbWin32_Superseded" Margin="0,5,0,5" Width="200" HorizontalAlignment="Left" />
<TextBlock Grid.Row="4" Grid.ColumnSpan="2" Text="Install time settings" FontWeight="SemiBold" Margin="0,8,0,4" />
<Label Grid.Row="5" Grid.Column="0" Content="Use local time:" />
<CheckBox Grid.Row="5" Grid.Column="1" Name="chkWin32_UseLocalTime" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="6" Grid.Column="0" Content="Start (ISO):" />
<TextBox Grid.Row="6" Grid.Column="1" Name="txtWin32_StartTime" Margin="0,5,0,5"
ToolTip="ISO8601 datetime, e.g. 2026-06-01T10:00:00Z. Leave empty for no start time." />
<Label Grid.Row="7" Grid.Column="0" Content="Deadline (ISO):" />
<TextBox Grid.Row="7" Grid.Column="1" Name="txtWin32_DeadlineTime" Margin="0,5,0,5" />
<TextBlock Grid.Row="8" Grid.ColumnSpan="2" Text="Restart settings (Required intent only)" FontWeight="SemiBold" Margin="0,8,0,4" />
<Label Grid.Row="9" Grid.Column="0" Content="Grace period (min):" />
<TextBox Grid.Row="9" Grid.Column="1" Name="txtWin32_GracePeriod" Margin="0,5,0,5" Width="120" HorizontalAlignment="Left" />
<Label Grid.Row="10" Grid.Column="0" Content="Countdown (min):" />
<TextBox Grid.Row="10" Grid.Column="1" Name="txtWin32_Countdown" Margin="0,5,0,5" Width="120" HorizontalAlignment="Left" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== winGet ======================== -->
<TabItem Header="winGet" Name="tabWinGet">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyWinGet" Content="Apply these settings to winGet apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyWinGet, Path=IsChecked}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Notifications:" />
<ComboBox Grid.Row="0" Grid.Column="1" Name="cbWinGet_Notifications" Margin="0,5,0,5" Width="200" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Grid.ColumnSpan="2" Text="Install time settings (deadline required when enabled)" FontWeight="SemiBold" Margin="0,8,0,4" />
<Label Grid.Row="2" Grid.Column="0" Content="Use local time:" />
<CheckBox Grid.Row="2" Grid.Column="1" Name="chkWinGet_UseLocalTime" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="3" Grid.Column="0" Content="Deadline (ISO):" />
<TextBox Grid.Row="3" Grid.Column="1" Name="txtWinGet_DeadlineTime" Margin="0,5,0,5" />
<TextBlock Grid.Row="4" Grid.ColumnSpan="2" Text="Restart settings (Required intent only)" FontWeight="SemiBold" Margin="0,8,0,4" />
<Label Grid.Row="5" Grid.Column="0" Content="Grace period (min):" />
<TextBox Grid.Row="5" Grid.Column="1" Name="txtWinGet_GracePeriod" Margin="0,5,0,5" Width="120" HorizontalAlignment="Left" />
<Label Grid.Row="6" Grid.Column="0" Content="Countdown (min):" />
<TextBox Grid.Row="6" Grid.Column="1" Name="txtWinGet_Countdown" Margin="0,5,0,5" Width="120" HorizontalAlignment="Left" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== Health Script (Proactive Remediations) ======================== -->
<TabItem Header="Health Script" Name="tabHealthScript">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyHealthScript" Content="Apply these settings to Health Script (Proactive Remediation) assignments" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyHealthScript, Path=IsChecked}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Run remediation:" />
<CheckBox Grid.Row="0" Grid.Column="1" Name="chkScript_RunRemediation" VerticalAlignment="Center" Margin="0,5,0,5"
ToolTip="True = run the remediation script when detection finds an issue. False = detection only." />
<Label Grid.Row="1" Grid.Column="0" Content="Schedule type:" />
<ComboBox Grid.Row="1" Grid.Column="1" Name="cbScript_ScheduleType" Margin="0,5,0,5" Width="200" HorizontalAlignment="Left" />
<Label Grid.Row="2" Grid.Column="0" Content="Interval:" />
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<TextBox Name="txtScript_Interval" Margin="0,5,5,5" Width="80"
ToolTip="Hourly: every N hours. Daily: every N days. Once: ignored." />
<TextBlock Text="(hours / days / once)" VerticalAlignment="Center" Foreground="{DynamicResource TextColor}" Opacity="0.6" />
</StackPanel>
<Label Grid.Row="3" Grid.Column="0" Content="Time (HH:MM):" />
<TextBox Grid.Row="3" Grid.Column="1" Name="txtScript_Time" Margin="0,5,0,5" Width="100" HorizontalAlignment="Left"
ToolTip="24-hour HH:MM. Ignored when schedule type is Hourly." />
<Label Grid.Row="4" Grid.Column="0" Content="Use UTC:" />
<CheckBox Grid.Row="4" Grid.Column="1" Name="chkScript_UseUtc" VerticalAlignment="Center" Margin="0,5,0,5"
ToolTip="Off = device local time. Ignored when schedule type is Hourly." />
<Label Grid.Row="5" Grid.Column="0" Content="Date (YYYY-MM-DD):" />
<TextBox Grid.Row="5" Grid.Column="1" Name="txtScript_Date" Margin="0,5,0,5" Width="150" HorizontalAlignment="Left"
ToolTip="Required for Once schedule type. Ignored otherwise." />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ======================== Windows AutoUpdate Catalog ======================== -->
<TabItem Header="Win AutoUpdate" Name="tabWinAutoUpdate">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8">
<CheckBox Name="chkApplyWinAutoUpdate" Content="Apply these settings to Windows AutoUpdate Catalog apps" FontWeight="SemiBold" Margin="0,0,0,8" />
<Grid IsEnabled="{Binding ElementName=chkApplyWinAutoUpdate, Path=IsChecked}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" SharedSizeGroup="SettingsLabel"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Delivery optimization:" />
<ComboBox Grid.Row="0" Grid.Column="1" Name="cbWinAutoUpdate_DeliveryOpt" Margin="0,5,0,5" Width="200" HorizontalAlignment="Left" />
<Label Grid.Row="1" Grid.Column="0" Content="Notification type:" />
<ComboBox Grid.Row="1" Grid.Column="1" Name="cbWinAutoUpdate_Notifications" Margin="0,5,0,5" Width="200" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.ColumnSpan="2" Text="Install time settings" FontWeight="SemiBold" Margin="0,8,0,4" />
<Label Grid.Row="3" Grid.Column="0" Content="Use local time:" />
<CheckBox Grid.Row="3" Grid.Column="1" Name="chkWinAutoUpdate_UseLocalTime" VerticalAlignment="Center" Margin="0,5,0,5" />
<Label Grid.Row="4" Grid.Column="0" Content="Start (ISO):" />
<TextBox Grid.Row="4" Grid.Column="1" Name="txtWinAutoUpdate_StartTime" Margin="0,5,0,5" />
<Label Grid.Row="5" Grid.Column="0" Content="Deadline (ISO):" />
<TextBox Grid.Row="5" Grid.Column="1" Name="txtWinAutoUpdate_DeadlineTime" Margin="0,5,0,5" />
<TextBlock Grid.Row="6" Grid.ColumnSpan="2" Text="Restart settings (Required intent only)" FontWeight="SemiBold" Margin="0,8,0,4" />
<Label Grid.Row="7" Grid.Column="0" Content="Grace period (min):" />
<TextBox Grid.Row="7" Grid.Column="1" Name="txtWinAutoUpdate_GracePeriod" Margin="0,5,0,5" Width="120" HorizontalAlignment="Left" />
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0">
<Button Name="btnBulkAssignSettingsOK" Content="OK" MinWidth="100" Margin="0,0,5,0" />
<Button Name="btnBulkAssignSettingsCancel" Content="Cancel" MinWidth="100" />
</StackPanel>
</Grid>
+135
View File
@@ -0,0 +1,135 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope='True'>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Name="grdImportProperties">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0">
<Label Content="Compare object" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the objects to compare e.g. compare exported files with existing objects or two existing objects based on name" />
</StackPanel>
<ComboBox Name="cbCompareProvider" Margin="0,5,0,0" MinWidth="250" Grid.Row='1' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
</Grid>
<ContentControl Name="ccContentProviderOptions" Grid.Row="1" Grid.ColumnSpan="2" />
<Grid Grid.Row='2' VerticalAlignment="Stretch" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='0'>
<Label Content="Save as" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specifies how the export csv should be saved. One file per ObjectType or one file for all objects." />
</StackPanel>
<ComboBox Name="cbCompareSave" Margin="0,5,0,0" MinWidth="250" Grid.Row='0' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='1' >
<Label Content="Output format" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the output file format for compare results" />
</StackPanel>
<ComboBox Name="cbCompareOutputFormat" Margin="0,5,0,0" MinWidth="250" Grid.Row='1' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='2' >
<Label Content="Comparison Type" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify how objects should be compared" />
</StackPanel>
<ComboBox Name="cbCompareType" Margin="0,5,0,0" MinWidth="250" Grid.Row='2' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='3' >
<Label Content="CSV Delimiter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the character to use for separating properties in the CSV file" />
</StackPanel>
<ComboBox Name="cbCompareCSVDelimiter" Margin="0,5,0,0" MinWidth="250" Grid.Row='3' Grid.Column="1" HorizontalAlignment="Left" />
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='4' >
<Label Content="Multi-Value Delimiter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the character to use for separating values for properties with more than one value. New line will cause a new line in the exported CSV" />
</StackPanel>
<ComboBox Name="cbCompareMultiValueDelimiter" Margin="0,5,0,0" MinWidth="250" Grid.Row='4' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row='5' >
<Label Content="Ignore core properties" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Skip mismatch coloring for displayName, description, id, createdDateTime, lastModifiedDateTime and version. These will almost always differ between two objects." />
</StackPanel>
<CheckBox Name="chkIgnoreCoreProperties" Grid.Row='5' Grid.Column="1" VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row='6' >
<Label Content="Skip Assignments" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Skip comparing assignments." />
</StackPanel>
<CheckBox Name="chkSkipCompareAssignments" Grid.Row='6' Grid.Column="1" VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row='7' >
<Label Content="Skip Missing Source Policies" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Skip policies that only exist on the compare side (e.g. in Intune but never exported)." />
</StackPanel>
<CheckBox Name="chkSkipMissingSourcePolicies" Grid.Row='7' Grid.Column="1" VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row='8' >
<Label Content="Skip Missing Destination Policies" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Skip policies whose counterpart is missing (e.g. exported file with no matching object in Intune)." />
</StackPanel>
<CheckBox Name="chkSkipMissingDestinationPolicies" Grid.Row='8' Grid.Column="1" VerticalAlignment="Center" />
<Grid Name="grdObjectsToCompareSection" Margin="0,0,5,0" Grid.Row='9' Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<Label Content="Objects to compare" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select the object types that should be compared" />
</StackPanel>
<DataGrid Name="dgObjectsToCompare" Margin="0,5,0,5" Grid.Row='1' Grid.Column='1' CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{DynamicResource ControlBackgroundColor}" />
</Grid>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='3' Grid.ColumnSpan='2' >
<Button Name="btnStartCompare" Content="Compare" Width='100' Margin="5,0,0,0" />
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid >
+40
View File
@@ -0,0 +1,40 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope='True'>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row='0' Margin="0,0,5,0" >
<Label Content="Source object name pattern" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the pattern of the source objects e.g. Test -" />
</StackPanel>
<TextBox Text="" Name="txtCopyFromPattern" Grid.Column='1' Grid.Row='0' Margin="0,5,0,0" />
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" >
<Label Content="Copy object name pattern" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the pattern of the new object e.g. Prod -" />
</StackPanel>
<TextBox Text="" Name="txtCopyToPattern" Grid.Column='1' Grid.Row='1' Margin="0,5,0,0" />
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="5,0,5,0" VerticalAlignment="Top" Grid.ColumnSpan="2">
<Label Content="Objects to copy" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select the object types that should be copied" Margin="0,2,0,0" />
</StackPanel>
<DataGrid Name="dgBulkCopyObjects" Grid.Row='3' Grid.ColumnSpan="2" CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{DynamicResource ControlBackgroundColor}" Margin="0,0,0,5">
</DataGrid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='4' Grid.ColumnSpan='2' >
<Button Name="btnStartCopy" Content="Copy" Width='100' Margin="5,0,0,0" />
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid >
+33
View File
@@ -0,0 +1,33 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope='True'>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row='0' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to delete" />
</StackPanel>
<TextBox Text="" Name="txtDeleteNameFilter" Grid.Column='1' Grid.Row='0' Margin="0,5,0,0" />
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="5,0,5,0" VerticalAlignment="Top" Grid.ColumnSpan="2">
<Label Content="Objects to delete" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="All objects of the selected types will be deleted" Margin="0,2,0,0" />
</StackPanel>
<DataGrid Name="dgBulkDeleteObjects" Grid.Row='2' Grid.ColumnSpan="2" CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{DynamicResource ControlBackgroundColor}" Margin="0,0,0,5">
</DataGrid>
<StackPanel Name="spDeleteSubMenu" Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='3' Grid.ColumnSpan='2' >
<Button Name="btnDelete" Content="Delete" Width='100' Margin="5,0,0,0" />
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid >
File diff suppressed because one or more lines are too long
+143
View File
@@ -0,0 +1,143 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope='True'>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Name="grdExportProperties">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" >
<Label Content="Export root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The root folder where exported files will be stored" />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='0'>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="{Binding ExportFolder, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="txtExportPath" />
<Button Grid.Column="2" Name="browseExportPath" Padding="5,0,5,0" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to export" />
</StackPanel>
<TextBox Text="{Binding Filter, Mode=TwoWay}" Name="txtExportNameFilter" Grid.Column='1' Grid.Row='1' Margin="0,5,0,0" />
<!-- Force object type in name by setting it to true and disable the checkbox. Leave it on for information -->
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0">
<Label Content="Add object name to path" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will export all objects to a sub-directory of the export path with name based on object type" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='2' Name='chkAddObjectType' VerticalAlignment="Center" IsEnabled="false" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='3' Margin="0,0,5,0">
<Label Content="Export Assignments" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Export object assignments" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='3' Name='chkExportAssignments' VerticalAlignment="Center" IsChecked="{Binding ExportAssignments, Mode=TwoWay}" />
<StackPanel Orientation="Horizontal" Grid.Row='4' Margin="0,0,5,0">
<Label Content="Add company name to path" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will add the company name in Azure to the export path" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='4' Name='chkAddCompanyName' VerticalAlignment="Center" IsChecked="{Binding AddCompanyName}" />
<StackPanel Orientation="Horizontal" Grid.Row='5' Margin="0,0,5,0">
<Label Content="Export nested group levels" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Depth of group-membership recursion. 1 (default) exports only the group directly assigned to the policy. 2 also exports groups that are direct members of the assigned group. Higher values mean more Graph calls and may hit throttling on large group trees." />
</StackPanel>
<TextBox Grid.Column='1' Grid.Row='5' Name='txtNestedGroupLevels' Width='60' HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{Binding ExportNestedGroupLevels, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel Orientation="Horizontal" Grid.Row='6' Margin="0,0,5,0">
<Label Content="List by" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Group: pick whole categories (Configuration, Compliance, ...). API: pick individual policy types." />
</StackPanel>
<StackPanel Grid.Column='1' Grid.Row='6' Orientation="Horizontal" VerticalAlignment="Center">
<RadioButton Name='rbBulkExportViewGroup' Content="Group" GroupName="BulkExportView" IsChecked="True" VerticalAlignment="Center" />
<RadioButton Name='rbBulkExportViewType' Content="API" GroupName="BulkExportView" Margin="15,0,0,0" VerticalAlignment="Center" />
</StackPanel>
</Grid>
<Grid Grid.Row='1' VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Margin="0,0,5,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="Objects to export" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select the object types that should be exported" />
</StackPanel>
</Grid>
<DataGrid Name="dgObjectsToExport" Grid.Column='1' CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{DynamicResource ControlBackgroundColor}" Margin="0,0,0,5" />
<!--
<ListBox Name="lstObjectsToExport" Grid.Column='1'
SelectionMode="Single"
Grid.IsSharedSizeScope='True' >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="SelectedColumn" />
<ColumnDefinition Width="Auto" SharedSizeGroup="FileNameColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding Title}" Grid.Column='1' Margin="5,0,0,0" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<CheckBox IsChecked="true" Margin="7,2,0,0" Grid.Column='1' Grid.Row='1' Name="chkCheckAll" ToolTip="Select/Deselect all" />
-->
</Grid >
<Grid Grid.Row='2' Grid.ColumnSpan='2' >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" >
</StackPanel>
<StackPanel Name="spExportSubMenu" Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Right" >
<Button Name="btnExport" Content="Export" Width='100' Margin="5,0,0,0" />
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid >
</Grid >
+136
View File
@@ -0,0 +1,136 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope='True'>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Name="grdImportProperties">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" >
<Label Content="Import root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The root folder where exported files are stored" />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='0'>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="" Name="txtImportPath" />
<Button Grid.Column="2" Name="browseImportPath" Padding="5,0,5,0" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to import" />
</StackPanel>
<TextBox Text="" Name="txtImportNameFilter" Grid.Column='1' Grid.Row='1' Margin="0,5,0,0" />
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0" Name="spMigrationTableInfo">
<Label Content="Migration Table" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This contains information about the exported environment e.g. Groups, ScopeTags etc. Note: This is only used when importing objects from a different tenant" />
</StackPanel>
<Label Grid.Column='1' Grid.Row='2' Name="lblMigrationTableInfo" />
<!-- Force object type in name by setting it to true and disable the checkbox. Leave it on for information -->
<StackPanel Orientation="Horizontal" Grid.Row='3' Margin="0,0,5,0">
<Label Content="Add object name to path" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will import objects from a sub-directory of the import path with name based on object type" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='3' Name='chkAddObjectType' VerticalAlignment="Center" IsEnabled="false" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='4' Margin="0,0,5,0">
<Label Content="Assign Scope (Tags)" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will import ScopeTags. The ScopeTags must exist in the target environment before they can be assigned during import of an object" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='4' Name='chkImportScopes' VerticalAlignment="Center" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='5' Margin="0,0,5,0">
<Label Content="Import Assignments" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Import object assignments. Note: This will create groups that don't exist in the target environment" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='5' Name='chkImportAssignments' VerticalAlignment="Center" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='6' Margin="0,0,5,0">
<Label Content="Replace Dependency IDs" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Replaces IDs of dependency objects e.g. App Config references Applications. Increases import time but makes sure objects are imported correctly. Note: Referenced objects must exist!" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='6' Name='chkReplaceDependencyIDs' VerticalAlignment="Center" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='7' Margin="0,0,5,0" Name="lblImportType" Visibility="Collapsed">
<Label Content="Import Type" />
<Rectangle Style="{DynamicResource InfoIcon}">
<Rectangle.ToolTip>
<TextBlock>
Specify how files are imported<LineBreak />
<Bold>Always import</Bold>: Always try to import the object. No detction of existing objects (Default)<LineBreak />
<Bold>Skip if object exists</Bold>: Skip import if there is an existing object with the same name and type<LineBreak />
<Bold>Replace</Bold>: If an object is detected, it will be deleted. The assignments will be copied to the new imported object<LineBreak />
<Bold>Replace with assignments</Bold>: If an object is detected, it will be deleted. The assignments will be copied from the imported file<LineBreak />
<Bold>Update</Bold>: If an object is detected, settings will be replaced from the import file
</TextBlock>
</Rectangle.ToolTip>
</Rectangle>
</StackPanel>
<ComboBox Name="cbImportType" Margin="0,5,0,0" MinWidth="250" Grid.Row='7' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" Visibility="Collapsed" />
</Grid>
<Grid Grid.Row='1' VerticalAlignment="Stretch" Margin="0,5,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Margin="0,0,5,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="Objects to import" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select the object types that should be imported" />
</StackPanel>
</Grid>
<DataGrid Name="dgObjectsToImport" Grid.Column='1' CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{DynamicResource ControlBackgroundColor}" Margin="0,0,0,5" />
</Grid>
<Grid Grid.Row='2' Grid.ColumnSpan='2' >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" >
</StackPanel>
<StackPanel Name="spImportSubMenu" Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Right" >
<Button Name="btnImport" Content="Import" Width='100' Margin="5,0,0,0" />
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid>
</Grid >
+125
View File
@@ -0,0 +1,125 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,5,0">
<Label Content="Action" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Add: union of current and selected. Replace: overwrite. Remove: subtract selected from current." />
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5,0,5">
<RadioButton Name="rbBulkScopeTagAdd" Content="Add" GroupName="BulkScopeTagAction" IsChecked="True" VerticalAlignment="Center" />
<RadioButton Name="rbBulkScopeTagReplace" Content="Replace" GroupName="BulkScopeTagAction" Margin="15,0,0,0" VerticalAlignment="Center" />
<RadioButton Name="rbBulkScopeTagRemove" Content="Remove" GroupName="BulkScopeTagAction" Margin="15,0,0,0" VerticalAlignment="Center" />
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="1" Margin="0,0,5,0">
<Label Content="Cleanup orphans" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="In addition to the chosen action, strip any tag ids from each policy that no longer resolve to a real scope tag in the tenant. Can also be used standalone (Add with no tags selected)." />
</StackPanel>
<CheckBox Grid.Column="1" Grid.Row="1" Name="chkBulkScopeTagCleanupOrphans" VerticalAlignment="Center" Margin="0,5,0,5"
IsChecked="{Binding CleanupOrphans, Mode=TwoWay}" />
<StackPanel Orientation="Horizontal" Grid.Row="2" Margin="0,0,5,0">
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Regex (or literal substring) matched against each policy's Name. Leave empty to target every listed policy." />
</StackPanel>
<TextBox Grid.Column="1" Grid.Row="2" Name="txtBulkScopeTagNameFilter" Margin="0,5,0,5"
Text="{Binding Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel Orientation="Horizontal" Grid.Row="3" Margin="0,0,5,0">
<Label Content="List by" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Group: pick whole categories (Configuration, Compliance, ...). API: pick individual policy types." />
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5,0,5">
<RadioButton Name="rbBulkScopeTagViewGroup" Content="Group" GroupName="BulkScopeTagView" IsChecked="True" VerticalAlignment="Center" />
<RadioButton Name="rbBulkScopeTagViewType" Content="API" GroupName="BulkScopeTagView" Margin="15,0,0,0" VerticalAlignment="Center" />
</StackPanel>
</Grid>
<Grid Grid.Row="1" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,3">
<Label Content="Objects to update" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select the policy types / groups that should be updated" />
</StackPanel>
<DataGrid Grid.Row="1" Name="dgBulkScopeTagObjects" CanUserAddRows="False" AutoGenerateColumns="False"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Background="{DynamicResource ControlBackgroundColor}" />
</Grid>
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,3">
<Label Content="Available scope tags" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Double-click to add to the Selected list" />
</StackPanel>
<ListBox Grid.Row="1" Name="lstBulkScopeTagAvailable" DisplayMemberPath="Name" SelectionMode="Extended"
Background="{DynamicResource ControlBackgroundColor}" />
<Grid Grid.Row="2" Margin="0,3,0,3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="Selected scope tags" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Tags to use with the chosen action. Double-click a row to remove it." />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Button Name="btnBulkScopeTagAddTag" Content="Add &#8595;" MinWidth="80" Margin="0,0,5,0"
ToolTip="Move the highlighted available tags to the Selected list" />
<Button Name="btnBulkScopeTagRemoveTag" Content="Remove &#8593;" MinWidth="80"
ToolTip="Move the highlighted selected tags back to the Available list" />
</StackPanel>
</Grid>
<ListBox Grid.Row="3" Name="lstBulkScopeTagSelected" DisplayMemberPath="Name" SelectionMode="Extended"
Background="{DynamicResource ControlBackgroundColor}" />
</Grid>
</Grid>
<Grid Grid.Row="2" Margin="0,5,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Name="txtBulkScopeTagStatus" VerticalAlignment="Center"
Foreground="{DynamicResource TextColor}" Opacity="0.8" />
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Name="btnBulkScopeTagApply" Content="Apply" MinWidth="100" Margin="5,0,0,0" />
<Button Name="btnBulkScopeTagClose" Content="Close" MinWidth="100" Margin="5,0,0,0" />
</StackPanel>
</Grid>
</Grid>
+78
View File
@@ -0,0 +1,78 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="4*" MinHeight="300" />
<RowDefinition Height="3" />
<RowDefinition Height="1*" MinHeight="100" />
</Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
Background="{DynamicResource PanelBackgroundColor}" Padding="8" Margin="0,0,0,5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<TextBlock Text="Entries: " FontWeight="Bold" Margin="0,0,2,0" />
<TextBlock Name="txtCacheEntries" Text="0" Margin="0,0,16,0" />
<TextBlock Text="Hits: " FontWeight="Bold" Margin="0,0,2,0" />
<TextBlock Name="txtCacheHits" Text="0" Margin="0,0,16,0" />
<TextBlock Text="Misses: " FontWeight="Bold" Margin="0,0,2,0" />
<TextBlock Name="txtCacheMisses" Text="0" Margin="0,0,16,0" />
<TextBlock Text="Total size: " FontWeight="Bold" Margin="0,0,2,0" />
<TextBlock Name="txtCacheBytes" Text="0 B" Margin="0,0,16,0"
ToolTip="Approximate in-memory size of all cached values (UTF-8 JSON serialization, compressed)." />
</StackPanel>
<Button Grid.Column="1" Name="btnRefreshCachedObjects" Content="Refresh"
Padding="8,2" Margin="5,0,0,0" VerticalAlignment="Center"
ToolTip="Recompute cache totals and reload the list" />
</Grid>
</Border>
<DataGrid Grid.Row="1" Name="dgCachedObjects"
AutoGenerateColumns="False"
SelectionMode="Single"
SelectionUnit="FullRow"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Tags" Binding="{Binding TagsText}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Size" Binding="{Binding SizeText}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Persistent" Binding="{Binding Persistent}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="TimeOut" Binding="{Binding TimeOut, StringFormat={}{0:HH:mm.ss.fff}}" IsReadOnly="True" Width="Auto" />
<DataGridTemplateColumn Header="Value">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Value}" TextWrapping="NoWrap" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<Grid Grid.Row="3" DataContext="{Binding Path=SelectedItem, ElementName=dgCachedObjects}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Name" Grid.Row="0" Grid.Column="0" />
<TextBlock Text="{Binding Path=Name}" Grid.Row="0" Grid.Column="2" />
<Button Name="btnClearCachedObject" Content="Clear" Grid.Row="0" Grid.Column="3" Margin="0,5,5,5" Width="100" ToolTip="Clear the selected object from cache" />
<TextBox Grid.Row="1" Grid.ColumnSpan="99" HorizontalAlignment="Stretch" Name="txtCacheObjectInfo" IsReadOnly="True" AcceptsReturn="true" />
</Grid>
</Grid>
+50
View File
@@ -0,0 +1,50 @@
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Choose Cloud"
SizeToContent="WidthAndHeight"
ResizeMode="NoResize"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=cbCloud}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="UI\WPF\Themes\Light.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Width="380" HorizontalAlignment="Stretch" Name="grdModalContainer" VerticalAlignment="Stretch" Background="{DynamicResource WindowBackgroundColor}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row="0">
<Label Content="Cloud" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select the Microsoft cloud to sign in to: Public (Global), US Government (GCC High), US Government (DoD), or China (Vianet)." />
</StackPanel>
<ComboBox Name="cbCloud" Margin="0,5,5,0" Grid.Row="0" Grid.Column="1"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Grid.Row='1' Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,5,5,5">
<Button Name="btnLogin" Content="Sign in" Width='100' Margin="5,0,0,0" VerticalAlignment="Center" IsDefault="True" />
<Button Name="btnCancel" Content="Cancel" Width='100' Margin="5,0,0,0" VerticalAlignment="Center" IsCancel="True" />
</StackPanel>
</Grid>
</Grid>
</Window>
+33
View File
@@ -0,0 +1,33 @@
<Grid Name="grdImportProperties" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row='0' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to compare" />
</StackPanel>
<TextBox Text="{Binding NameFilter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="txtCompareNameFilter" Grid.Column='1' Grid.Row='0' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" >
<Label Content="Export root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The root folder where exported files are stored. This sould be the Company Name folder if it was included in the export." />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='1' Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="{Binding ExportPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="txtExportPath" />
<Button Grid.Column="2" Name="browseExportPath" Padding="5,2,5,2" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
</Grid>
@@ -0,0 +1,51 @@
<Grid Name="grdImportProperties" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row='0' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to compare" />
</StackPanel>
<TextBox Text="{Binding NameFilter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="txtCompareNameFilter" Grid.Column='1' Grid.Row='0' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" >
<Label Content="Source root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The root folder where exported files are stored. This sould be the Company Name folder if it was included in the export." />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='1' Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="{Binding SourcePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="txtExportPathSource" />
<Button Grid.Column="2" Name="browseExportPathSource" Padding="5,2,5,2" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0" >
<Label Content="Compare root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The folder with exported object that should be compared with the source" />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='2' Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="{Binding ComparePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="txtExportPathCompare" />
<Button Grid.Column="2" Name="browseExportPathCompare" Padding="5,2,5,2" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
</Grid>
+9
View File
@@ -0,0 +1,9 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Name="txtCompareFile" />
<Button Grid.Column="2" Name="browseCompareObject" Padding="5,2,5,2" Width="50" ToolTip="Browse for file">...</Button>
</Grid>
+198
View File
@@ -0,0 +1,198 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TabControl Name="tabCompare" SelectedIndex="0" Margin="0,0,0,5">
<TabItem Header="Options">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Padding="0,0,5,0">
<Grid VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row='0' Margin="0,5,5,0">
<Label Content="Source object" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Current Intune object" />
</StackPanel>
<TextBlock Name="txtIntuneObject" Text="" Grid.Column='1' Grid.Row='0' Margin="0,9,5,0"/>
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,5,5,0">
<Label Content="Compare with" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select whether to compare against an exported file or another Intune object" />
</StackPanel>
<ComboBox Name="cbCompareInput" Margin="0,5,5,0" MinWidth="200" Grid.Row='1' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,5,5,0">
<Label Name="lblCompareObject" Content="Compare object" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The object to compare against" />
</StackPanel>
<ContentControl Name="ccCompareInputOptions" Grid.Column='1' Grid.Row='2' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Grid.Row='3' Margin="0,5,5,0">
<Label Content="Comparison Type" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify how objects should be compared" />
</StackPanel>
<ComboBox Name="cbCompareType" Margin="0,5,5,0" MinWidth="250" Grid.Row='3' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Orientation="Horizontal" Grid.Row='4' Margin="0,5,5,0">
<Label Content="Output format" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the output file format when saving compare results" />
</StackPanel>
<ComboBox Name="cbSingleCompareOutputFormat" Margin="0,5,5,0" MinWidth="200" Grid.Row='4' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Orientation="Horizontal" Grid.Row='5' Margin="0,5,5,0">
<Label Content="Ignore core properties" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Skip mismatch coloring for displayName, description, id, createdDateTime, lastModifiedDateTime and version. These will almost always differ between two objects." />
</StackPanel>
<CheckBox Name="chkIgnoreCoreProperties" Margin="0,9,5,0" Grid.Row='5' Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal" Grid.Row='6' Margin="0,5,5,0">
<Label Content="Multi-Value Delimiter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the character to use for separating values for properties with more than one value (documentation comparison)" />
</StackPanel>
<ComboBox Name="cbCompareMultiValueDelimiter" Margin="0,5,5,0" MinWidth="200" Grid.Row='6' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Orientation="Horizontal" Grid.Row='7' Margin="0,5,5,0">
<Label Content="Skip Assignments" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Skip comparing assignments." />
</StackPanel>
<CheckBox Name="chkSkipCompareAssignments" Margin="0,9,5,0" Grid.Row='7' Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" />
<TextBlock Grid.Row='8' TextWrapping="Wrap" Margin="5,20,5,0" Grid.ColumnSpan="2">
The <Bold>Property</Bold> comparison method is a quick and easy option for objects that are based on a static set of properties <LineBreak/>
e.g. Configuration Profiles, Compliance Polcies etc.<LineBreak/>This method will compare each property of the objects. <LineBreak/>
The downside of this method is that it could be difficult to match the property name with the setting in th UI.
<LineBreak/>
The <Bold>Documentation</Bold> method will use the documentation engine and compare property values as they are displayed in the portal.<LineBreak/>
This is recommended for objects with dynamic settings e.g. Settings Catalog and Endpoint security objects. <LineBreak/>
The downside of this method is that it could be slow and that some property-based objects are not supported.
</TextBlock>
</Grid>
</ScrollViewer>
</TabItem>
<TabItem Header="Compare Output">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DockPanel Grid.Row='0' LastChildFill="False" Margin="0,0,0,5">
<TextBlock Text="Show:" VerticalAlignment="Center" Margin="5,0,5,0" DockPanel.Dock="Left" />
<ComboBox Name="cbCompareFilter" Width="160" VerticalAlignment="Center" DockPanel.Dock="Left">
<ComboBoxItem Content="All properties" Tag="All" />
<ComboBoxItem Content="Mismatches only" Tag="Mismatch" />
<ComboBoxItem Content="Matches only" Tag="Match" />
</ComboBox>
<TextBlock Name="txtCompareSummary" Text="" VerticalAlignment="Center" Margin="20,0,0,0" Foreground="{DynamicResource TextColor}" DockPanel.Dock="Left" />
</DockPanel>
<DataGrid Grid.Row='1' Name="dgCompareInfo"
AutoGenerateColumns="False"
SelectionMode="Single"
SelectionUnit="FullRow"
CanUserAddRows="False"
VirtualizingPanel.ScrollUnit="Pixel">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<!-- Selection highlight brushes come from the theme dictionary
(Themes/Light.xaml, Themes/Dark.xaml). They were redefined here,
hardcoded to the dark palette, which made a selected row unreadable
in the Light theme. -->
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SelectedRowBackgroundColor}" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource SelectedRowBackgroundColor}" />
</Trigger>
<!-- Match foreground tint at the ROW level. Cell-level
Foreground setters on a DataGridCell style do NOT
propagate to DataGridTextColumn's auto-generated
TextBlocks reliably (WPF wraps the cell content in a
ContentPresenter that bypasses cell.Foreground for
text columns). Setting Foreground on DataGridRow
flows through inheritance to every child TextBlock
and renders mismatched rows red as expected. Match
is tri-state ($true / $false / $null); only False
tints — True falls through to the inherited TextColor,
null (ignored) likewise stays neutral. -->
<DataTrigger Binding="{Binding Match}" Value="False">
<Setter Property="Foreground" Value="{DynamicResource MismatchColor}" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<!-- Match is tri-state on the data: $true / $false / $null. The
column converts each to a single-glyph string in XAML via a
DataTrigger chain so we don't need a code-behind converter
(the project loads the XAML at runtime — no codegen). -->
<DataGridTemplateColumn Header="Match" IsReadOnly="True" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock HorizontalAlignment="Center" FontWeight="Bold">
<TextBlock.Style>
<Style TargetType="TextBlock">
<!-- Default = ignored / skipped (Match is $null) -->
<Setter Property="Text" Value="—" />
<Setter Property="ToolTip" Value="Ignored" />
<Style.Triggers>
<DataTrigger Binding="{Binding Match}" Value="True">
<Setter Property="Text" Value="✓" />
<Setter Property="ToolTip" Value="Match" />
</DataTrigger>
<DataTrigger Binding="{Binding Match}" Value="False">
<Setter Property="Text" Value="✗" />
<Setter Property="ToolTip" Value="Mismatch" />
<Setter Property="Foreground" Value="{DynamicResource MismatchColor}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Property" Binding="{Binding PropertyName}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Category" Binding="{Binding Category}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="SubCategory" Binding="{Binding SubCategory}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Intune Object" Binding="{Binding Object1Value}" IsReadOnly="True" Width="1*" />
<DataGridTextColumn Header="Compare Object" Binding="{Binding Object2Value}" IsReadOnly="True" Width="1*" />
</DataGrid.Columns>
</DataGrid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='2' Margin="5,5,0,0">
<Button Name="btnCompareSave" Content="Save" ToolTip="Save compare results" Width='100' Margin="5,0,0,0" />
<Button Name="btnCompareCopy" Content="Copy" ToolTip="Copy CSV content to clipboard" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid>
</TabItem>
</TabControl>
<StackPanel Name="spCompareSubMenu" Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='1'>
<Button Name="btnSwap" Content="Swap" ToolTip="Swap source and compare objects" Width='100' Margin="5,0,0,0" />
<Button Name="btnStartCompare" Content="Compare" ToolTip="Compare objects" Width='100' Margin="5,0,0,0" />
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid>
@@ -0,0 +1,9 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Name="txtCompareIntuneObject" VerticalAlignment="Center" Margin="2,0,0,0" />
<Button Grid.Column="2" Name="browseIntuneObject" Padding="5,2,5,2" Width="50" ToolTip="Browse for Intune object">...</Button>
</Grid>
+40
View File
@@ -0,0 +1,40 @@
<Grid Name="grdImportProperties" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" >
<Label Content="Source object name pattern" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the pattern of the source objects e.g. Test -" />
</StackPanel>
<TextBox Text="{Binding SourcePattern, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="txtCompareSource" Grid.Column='1' Grid.Row='0' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='1'>
<Label Content="Compare object name pattern" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the pattern of the objects that the source should be compared with e.g. Prod -" />
</StackPanel>
<TextBox Text="{Binding ComparePattern, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="txtCompareWith" Grid.Column='1' Grid.Row='1' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row='2' >
<Label Content="Save folder" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The folder where compare files will be saved. Default is the MyDocuments folder" />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='2' Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="{Binding SavePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="txtSavePath" />
<Button Grid.Column="2" Name="browseSavePath" Padding="5,2,5,2" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
</Grid>
+93
View File
@@ -0,0 +1,93 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Copy object"
SizeToContent="WidthAndHeight"
ResizeMode="NoResize"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=txtObjectName}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="UI\WPF\Themes\Light.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row="0" HorizontalAlignment="Left">
<Label Content="Tenant" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Destination tenant" />
</StackPanel>
<ComboBox Name="cbDestinationTenant" Margin="0,5,5,0" Grid.Row="0" Grid.Column="1" Width="250">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TenantNameEx}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row="1" HorizontalAlignment="Left">
<Label Content="New Name" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Name of the object" />
</StackPanel>
<TextBox Name="txtObjectName" Margin="0,5,5,0" Grid.Row="1" Grid.Column="1" Width="250"/>
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row="2" HorizontalAlignment="Left" VerticalAlignment="Top" >
<Label Content="Description" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Description for the object" Margin="0,2,0,0" />
</StackPanel>
<TextBox Name="txtObjectDescription" Margin="0,5,5,0" Grid.Row="2" Grid.Column="1" AcceptsReturn="True" Width="250" Height="100" />
<StackPanel Name="pnlCopyScopeTagsLabel" Orientation="Horizontal" Margin="0,10,5,0" Grid.Row="3" Grid.ColumnSpan="2" HorizontalAlignment="Left">
<Label Content="Scope Tags" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Pick which scope tags the new object will have. Re-loads from the destination tenant when you change Tenant above; assignments are matched by name across tenants." />
</StackPanel>
<Grid Name="grdCopyScopeTags" Grid.Row="4" Grid.ColumnSpan="2" Height="180" Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Content="Available" Grid.Row="0" Grid.Column="0" Padding="0,0,0,2" FontSize="11" Foreground="#888"/>
<Label Content="Assigned" Grid.Row="0" Grid.Column="2" Padding="0,0,0,2" FontSize="11" Foreground="#888"/>
<ListBox Name="lstCopyAvailableScopeTags" Grid.Row="1" Grid.Column="0"
SelectionMode="Extended" DisplayMemberPath="Name" Margin="0,0,5,0"/>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Vertical" VerticalAlignment="Center" Margin="0,0,5,0">
<Button Name="btnCopyScopeTagAssign" Content="&gt;" Width="40" Margin="0,0,0,5" ToolTip="Assign selected tag(s)"/>
<Button Name="btnCopyScopeTagUnassign" Content="&lt;" Width="40" ToolTip="Unassign selected tag(s)"/>
</StackPanel>
<ListBox Name="lstCopyAssignedScopeTags" Grid.Row="1" Grid.Column="2"
SelectionMode="Extended" DisplayMemberPath="Name" Margin="5,0,0,0"/>
</Grid>
<WrapPanel Grid.Row="5" Grid.ColumnSpan="2" HorizontalAlignment="Right" Margin="0,15,0,0">
<Button IsDefault="True" Name="btnOk" MinWidth="60" Margin="0,0,10,0">_OK</Button>
<Button IsCancel="True" Name="btnCancel" MinWidth="60" >_Cancel</Button>
</WrapPanel>
</Grid>
</Window>
+27
View File
@@ -0,0 +1,27 @@
<Border xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" Padding="5" Background="{DynamicResource WindowBackgroundColor}">
<Grid HorizontalAlignment="Stretch" Name="grdModalContainer" VerticalAlignment="Stretch" Background="{DynamicResource WindowBackgroundColor}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Background="{DynamicResource TitleBackgroundColor}" BorderThickness="0">
<TextBlock Name="txtEditScriptTitle" Margin="5" FontWeight="Bold" />
</Border>
<TextBox Name="txtScriptText"
Grid.Row="1"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="True"
MinWidth="250" Margin="0,5,0,0" AcceptsReturn="True" />
<WrapPanel Name="pnlButtons" Grid.Row="2" HorizontalAlignment="Right" Margin="0,5,0,0">
<Button Name="btnSaveScriptEdit" MinWidth="100" Margin="0,0,5,0" ToolTip="Save updated script">Save</Button>
<Button Name="btnCancelScriptEdit" MinWidth="100" Margin="0,0,0,0" ToolTip="Cancel any changes">Cancel</Button>
</WrapPanel>
</Grid>
</Border>
+64
View File
@@ -0,0 +1,64 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope='True'>
<Grid Name="grdExportProperties">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" >
<Label Content="Export root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This root folder where exported files should be stored" />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='0'>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="{Binding ExportFolder, Mode=TwoWay}" Name="txtExportPath" />
<Button Grid.Column="2" Name="browseExportPath" Padding="5,0,5,0" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0">
<Label Content="Add object name to path" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will export all objects to a sub-directory of the export path with name based on object type" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='1' Name='chkAddObjectType' VerticalAlignment="Center" IsChecked="{Binding AddObjectType, Mode=TwoWay}" />
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0">
<Label Content="Add company name to path" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will add the company name in Azure to the export path" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='2' Name='chkAddCompanyName' VerticalAlignment="Center" IsChecked="{Binding AddCompanyName}" />
<StackPanel Orientation="Horizontal" Grid.Row='3' Margin="0,0,5,0">
<Label Content="Export Assignments" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will export assignments and add information to a migration table so they can be imported into other environments" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='3' Name='chkExportAssignments' VerticalAlignment="Center" IsChecked="{Binding ExportAssignments, Mode=TwoWay}" />
</Grid>
</StackPanel>
<StackPanel Name="spExportSubMenu" Grid.Row='1' Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,5,0,5">
<Label Name="lblSelectedObject" Margin="5,0,0,0" />
<Button Name="btnExportSelected" Content="Export Selected" Width='100' Margin="5,0,0,0" VerticalAlignment="Center" />
<Button Name="btnExportAll" Content="Export All" Width='100' Margin="5,0,0,0" VerticalAlignment="Center" />
<Button Name="btnCancel" Content="Cancel" Width='100' Margin="5,0,0,0" VerticalAlignment="Center" />
</StackPanel>
</Grid>
+125
View File
@@ -0,0 +1,125 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="4*" MinHeight="300" />
<RowDefinition Height="3" />
<RowDefinition Height="1*" MinHeight="100" />
</Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
Background="{DynamicResource PanelBackgroundColor}" Padding="8" Margin="0,0,0,5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal">
<TextBlock Text="Calls: " FontWeight="Bold" Margin="0,0,2,0" />
<TextBlock Name="txtTotalCalls" Text="0" Margin="0,0,16,0" />
<TextBlock Text="Downloaded: " FontWeight="Bold" Margin="0,0,2,0" />
<TextBlock Name="txtTotalMB" Text="0 MB" Margin="0,0,16,0" />
<TextBlock Text="Objects: " FontWeight="Bold" Margin="0,0,2,0" />
<TextBlock Name="txtTotalObjects" Text="0" Margin="0,0,16,0" />
<TextBlock Text="Total time: " FontWeight="Bold" Margin="0,0,2,0" />
<TextBlock Name="txtTotalTime" Text="0s" Margin="0,0,16,0" />
</StackPanel>
<Button Grid.Row="0" Grid.Column="1" Name="btnRefreshGraphCalls" Content="Refresh"
Padding="8,2" Margin="5,0,0,0" VerticalAlignment="Center"
ToolTip="Recompute totals and reload the call list" />
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock Text="HTTP status: " FontWeight="Bold" Margin="0,0,2,0" VerticalAlignment="Center" />
<WrapPanel Name="pnlHttpStatuses" Orientation="Horizontal" />
</StackPanel>
<StackPanel Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,2,0,0">
<TextBlock Text="Batch status: " FontWeight="Bold" Margin="0,0,2,0" VerticalAlignment="Center" />
<WrapPanel Name="pnlBatchStatuses" Orientation="Horizontal" />
</StackPanel>
<StackPanel Name="pnlFilterIndicator" Grid.Row="3" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,6,0,0" Visibility="Collapsed">
<TextBlock Text="Active filter: " FontWeight="Bold" Margin="0,0,2,0" VerticalAlignment="Center" />
<TextBlock Name="txtActiveFilter" Text="" Margin="0,0,8,0" VerticalAlignment="Center" />
<Button Name="btnClearGraphCallsFilter" Content="Clear" Padding="6,1" VerticalAlignment="Center"
ToolTip="Remove the filter and show all calls" />
</StackPanel>
</Grid>
</Border>
<DataGrid Grid.Row="1" Name="dgGraphCalls"
AutoGenerateColumns="False"
SelectionMode="Single"
SelectionUnit="FullRow"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Provider" Binding="{Binding Provider}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Date" Binding="{Binding Time, StringFormat={}{0:d}}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Time" Binding="{Binding Time, StringFormat={}{0:HH:mm.ss}}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Duration (ms)" Binding="{Binding Duration, StringFormat={}{0:N0}}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="KB" Binding="{Binding KB, StringFormat={}{0:N1}}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Objects" Binding="{Binding ObjectCount, StringFormat={}{0:N0}}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Pages" Binding="{Binding PageCount, StringFormat={}{0:N0}}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="StatusCode" Binding="{Binding StatusCode}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Batch Errors" Binding="{Binding BatchErrorSummary}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Method" Binding="{Binding Method}" IsReadOnly="True" Width="Auto" />
<DataGridTemplateColumn Header="URL">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=URL}" TextWrapping="NoWrap" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<Grid Grid.Row="3" Name="dgGraphCallObject">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="Batch info" Grid.Row="0" Grid.Column="0" />
<DataGrid
AutoGenerateColumns="False"
SelectionMode="Single"
SelectionUnit="FullRow"
CanUserAddRows="False"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
ItemsSource="{Binding Path=SelectedItem.BatchRequests, ElementName=dgGraphCalls}"
>
<DataGrid.Columns>
<DataGridTextColumn Header="Id" Binding="{Binding Id}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="KB" Binding="{Binding Response.KB, StringFormat={}{0:N1}}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Objects" Binding="{Binding Response.ObjectCount, StringFormat={}{0:N0}}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Pages" Binding="{Binding Response.PageCount, StringFormat={}{0:N0}}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="StatusCode" Binding="{Binding Response.StatusCode}" IsReadOnly="True" Width="Auto" />
<DataGridTextColumn Header="Method" Binding="{Binding Method}" IsReadOnly="True" Width="Auto" />
<DataGridTemplateColumn Header="URL">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding URL}" TextWrapping="NoWrap" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Grid>
+13
View File
@@ -0,0 +1,13 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="29" Width="33">
<Path Fill="#3E3E3E" Data="M23.1 21.5c0 1.4-1.1 2.5-2.5 2.5h-11c-1.4 0-2.5-1.1-2.5-2.5v-19C7.1 1.1 8.2 0 9.6 0h11c1.4 0 2.5 1.1 2.5 2.5v19z" />
<Path Fill="#5AB3D9" Data="M21.1 4h-12v17h12V4z" />
<Path Fill="#FFFFFF" Data="M7.1 2.5v16.3L21 0H9.6C8.2 0 7.1 1.1 7.1 2.5z" Opacity="0.15" />
<Path Fill="#FFFFFF" Data="M25.9 27.6V9.8h-8.5c-.7 0-1.3.6-1.3 1.3v16.4h9.8v.1z" Opacity="0.4" />
<Path Fill="#0072c6" Data="M29 11.6c-.2-.3-.6-.3-.6-.3H18.1s-.4 0-.5.3v20.1c0 .5.6.5.6.5h10.3s.6 0 .6-.5V11.8c-.1-.1-.1-.2-.1-.2z" />
<Path Fill="#FFFFFF" Data="M20.6 12.5h-1.8v3.6h1.8v-3.6zm3.5 0h-1.8v3.6h1.8v-3.6zm3.6 0h-1.8v3.6h1.8v-3.6zm-7.1 5.3h-1.8v3.6h1.8v-3.6zm3.5 0h-1.8v3.6h1.8v-3.6zm3.6 0h-1.8v3.6h1.8v-3.6zm-7.1 5.3h-1.8v3.6h1.8v-3.6zm3.5 0h-1.8v3.6h1.8v-3.6zm0 5.3h-1.8V32h1.8v-3.6zm3.6-5.3h-1.8v3.6h1.8v-3.6z" />
<Path Fill="#FFFFFF" Data="M9.6 22.2l-.1-.2c1.2-.8 2.1-2.2 2.1-3.7 0-2.1-1.4-3.8-3.4-4.3H8c-.2 0-.5-.1-.7-.1-2.4 0-4.4 2-4.4 4.4 0 .3 0 .6.1.9v.1c.3 1.2 1 2.1 1.9 2.7v.2H0V32h14.3v-9.8H9.6z" Opacity="0.4" />
<Path Fill="#804998" Data="M10.7 18.2c0 2-1.6 3.6-3.6 3.6s-3.6-1.6-3.6-3.6 1.6-3.6 3.6-3.6c2.1 0 3.6 1.7 3.6 3.6zM9.8 23l-2.6 3.7L4.6 23H.8v9h12.7v-9H9.8z" />
<Path Fill="#FFFFFF" Data="M3.6 18.2c0 2 1.5 3.5 3.5 3.6l.9-7c-.2-.1-.5-.1-.8-.1-2-.1-3.6 1.6-3.6 3.5zm1 4.8H.8v9h5l.8-6.1-2-2.9z" Opacity="0.15" />
</Canvas>
</Viewbox>
+9
View File
@@ -0,0 +1,9 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Fill="#1D1D1D" Data="M43.75 43.75H6.25A6.25 6.25 0 010 37.5v-25a6.25 6.25 0 016.25-6.25h37.5A6.25 6.25 0 0150 12.5v25a6.25 6.25 0 01-6.25 6.25z" />
<Path Fill="#B7D333" Data="M6.25 12.5h37.5v25H6.25z" />
<Path Fill="#7FBA42" Data="M43.75 37.5H6.25v-6.25l37.5-12.5z" />
<Path Fill="#FFFFFF" Data="M40.625 15.625v18.75H9.375v-18.75h31.25M43.75 12.5H6.25v25h37.5v-25z" Opacity="0.4" />
<Path Fill="#9E9FA0" Data="M18.75 37.5h12.5v3.125h-12.5z" />
</Canvas>
</Viewbox>
+6
View File
@@ -0,0 +1,6 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Fill="#2072B8" Data="M0 0h12.5v12.5H0V0zm0 15.625h12.5v12.5H0v-12.5zM0 31.25h12.5v12.5H0v-12.5zM15.625 0h12.5v12.5h-12.5V0zM31.25 0h12.5v12.5h-12.5V0zM16.788 38.29l3.822-3.422.008-1.332-2.637-2.286h-2.356v12.5h3.378zm2.276-13.546l5.126.288.946-.935-.362-5.086 3.351-1.364v-2.022h-12.5v12.5h2.012zm14.609-4.134l1.332.009 3.345-3.856 5.4 2.283v-3.421h-12.5v2.277z" />
<Path Fill="#7B7B7B" Data="M48.464 42.171l1.431-3.386-.414-.361-3.126-2.71.019-2.744 3.52-3.15-1.385-3.405-.543.038-4.13.292-1.926-1.955.262-4.714-3.386-1.431-.359.414-2.714 3.128-2.744-.019-3.15-3.522-3.405 1.385.04.543.29 4.13-1.951 1.928-4.718-.266-1.431 3.389.414.357 3.126 2.714-.017 2.744-3.519 3.15 1.382 3.405.547-.038 4.126-.292 1.928 1.951-.262 4.718 3.388 1.427.357-.41 2.714-3.128 2.742.023 3.15 3.519 3.405-1.385-.038-.546-.292-4.126 1.955-1.928 4.714.261zm-17.119-.968a7.522 7.522 0 01-4.006-9.857 7.522 7.522 0 019.857-4.008 7.522 7.522 0 014.007 9.857 7.524 7.524 0 01-9.858 4.008" />
</Canvas>
</Viewbox>
+7
View File
@@ -0,0 +1,7 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Fill="#2072B8" Data="M0 0h12.5v12.5H0V0zm0 15.625h12.5v12.5H0v-12.5zM0 31.25h12.5v12.5H0v-12.5zM15.625 0h12.5v12.5h-12.5V0zM31.25 0h12.5v12.5h-12.5V0zm-7.812 37.846V31.25h-7.813v12.5h11.161c-1.926-2.052-3.348-4.172-3.348-5.904zm0-18.989l1.492-.067a29.146 29.146 0 003.195-.344v-2.82h-12.5v12.5h7.813v-9.269zm11.853-3.232H31.25v2.081c1.477-.465 2.911-1.128 4.041-2.081zm8.459 2.08v-2.08h-4.035c1.133.952 2.563 1.614 4.035 2.08z" />
<Path Fill="#7FBA42" Data="M37.509 50C35.851 49.674 25 41.763 25 37.846V20.351c2.313-.103 9.37-.746 12.502-4.726 3.15 3.983 10.189 4.623 12.498 4.726v17.494C50 43.382 38.92 49.791 37.509 50z" />
<Path Data="M27.944 42.749C26.039 40.738 25 39.014 25 37.846V20.351c2.313-.103 9.369-.746 12.501-4.725 1.285 1.631 3.345 2.868 6.148 3.694L27.944 42.749z" Fill="#FFFFFF" Opacity="0.17"/>
</Canvas>
</Viewbox>
@@ -0,0 +1,26 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Fill="#FFFFFF" Data="M40.5 11h4v1h-4z" />
<Path Fill="#E5E5E5" Data="M20.5 49c-1.103 0-2-.897-2-2V7c0-1.103.897-2 2-2h24c1.103 0 2 .897 2 2v40c0 1.103-.897 2-2 2h-24z" />
<Path Fill="#7A7A7A" Data="M44.5 6c.551 0 1 .449 1 1v40c0 .551-.449 1-1 1h-24c-.551 0-1-.449-1-1V7c0-.551.449-1 1-1h24m0-2h-24a3 3 0 00-3 3v40a3 3 0 003 3h24a3 3 0 003-3V7a3 3 0 00-3-3z" />
<Path Fill="#FFFFFF" Data="M20.5 49c-1.103 0-2-.897-2-2V7c0-1.103.897-2 2-2h24c1.103 0 2 .897 2 2v40c0 1.103-.897 2-2 2h-24z" />
<Path Fill="#7A7A7A" Data="M44.5 6c.551 0 1 .449 1 1v40c0 .551-.449 1-1 1h-24c-.551 0-1-.449-1-1V7c0-.551.449-1 1-1h24m0-2h-24a3 3 0 00-3 3v40a3 3 0 003 3h24a3 3 0 003-3V7a3 3 0 00-3-3z" />
<Path Fill="#7A7A7A" Data="M19.5 6h26v6h-26z" />
<Path Fill="#FFFFFF" Data="M29.5 7h6v1h-6z" />
<Ellipse Fill="#57a300" Canvas.Left="23.5" Canvas.Top="14" Width="18" Height="18" />
<Path Fill="#0072c0" Data="M30.833 28.667v-4h-4v-3.334h4v-4h3.334v4h4v3.334h-4v4z" />
<Path Fill="#FFFFFF" Data="M37.5 22h-4v-4h-2v4h-4v2h4v4h2v-4h4z" />
<Path Fill="#3898C6" Data="M24.5 35h16v2h-16zm0 4h12v2h-12zm0 4h14v2h-14z" />
<Path Fill="#FFFFFF" Data="M25.5 7h4v1h-4z" />
<Path Fill="#E5E5E5" Data="M5.5 45c-1.103 0-2-.897-2-2V3c0-1.103.897-2 2-2h24c1.103 0 2 .897 2 2v40c0 1.103-.897 2-2 2h-24z" />
<Path Fill="#7A7A7A" Data="M29.5 2c.551 0 1 .449 1 1v40c0 .551-.449 1-1 1h-24c-.551 0-1-.449-1-1V3c0-.551.449-1 1-1h24m0-2h-24a3 3 0 00-3 3v40a3 3 0 003 3h24a3 3 0 003-3V3a3 3 0 00-3-3z" />
<Path Fill="#FFFFFF" Data="M5.5 45c-1.103 0-2-.897-2-2V3c0-1.103.897-2 2-2h24c1.103 0 2 .897 2 2v40c0 1.103-.897 2-2 2h-24z" />
<Path Fill="#7A7A7A" Data="M29.5 2c.551 0 1 .449 1 1v40c0 .551-.449 1-1 1h-24c-.551 0-1-.449-1-1V3c0-.551.449-1 1-1h24m0-2h-24a3 3 0 00-3 3v40a3 3 0 003 3h24a3 3 0 003-3V3a3 3 0 00-3-3z" />
<Path Fill="#7A7A7A" Data="M4.5 2h26v6h-26z" />
<Path Fill="#FFFFFF" Data="M14.5 3h6v1h-6z" />
<Ellipse Fill="#57a300" Canvas.Left="8.5" Canvas.Top="10" Width="18" Height="18" />
<Path Fill="#0072c0" Data="M15.833 24.667v-4h-4v-3.334h4v-4h3.334v4h4v3.334h-4v4z" />
<Path Fill="#FFFFFF" Data="M22.5 18h-4v-4h-2v4h-4v2h4v4h2v-4h4z" />
<Path Fill="#3898C6" Data="M9.5 31h16v2h-16zm0 4h12v2h-12zm0 4h14v2h-14z" />
</Canvas>
</Viewbox>
@@ -0,0 +1,60 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="18" Width="18">
<Canvas.Resources>
<LinearGradientBrush x:Key="3fe769a7-5a01-4d2d-8fe0-e8d731b660ed" StartPoint="0.0754000023007393,0.0494999997317791" EndPoint="0.0754000023007393,0.147200003266335" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0.22" Color="#2BCDF2" />
<GradientStop Offset="0.47" Color="#2ACAF0" />
<GradientStop Offset="0.63" Color="#28C2E7" />
<GradientStop Offset="0.76" Color="#24B3DA" />
<GradientStop Offset="0.88" Color="#1F9EC6" />
<GradientStop Offset="0.99" Color="#1883AD" />
<GradientStop Offset="1" Color="#177FA9" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="3fe769a7-5a01-4d2d-8fe0-e8d731b660ee" StartPoint="0.521659970283508,0" EndPoint="0.56852000951767,0.566280007362366" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0.22" Color="#2BCDF2" />
<GradientStop Offset="0.47" Color="#2ACAF0" />
<GradientStop Offset="0.63" Color="#28C2E7" />
<GradientStop Offset="0.76" Color="#24B3DA" />
<GradientStop Offset="0.88" Color="#1F9EC6" />
<GradientStop Offset="0.99" Color="#1883AD" />
<GradientStop Offset="1" Color="#177FA9" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="3fe769a7-5a01-4d2d-8fe0-e8d731b660ef" StartPoint="0.132200002670288,0.208000004291534" EndPoint="0.132200002670288,0.0612999983131886" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="#006DCD" />
<GradientStop Offset="0.11" Color="#1175D3" />
<GradientStop Offset="0.34" Color="#2E83DE" />
<GradientStop Offset="0.56" Color="#428DE6" />
<GradientStop Offset="0.79" Color="#4F93EA" />
<GradientStop Offset="1" Color="#5395EC" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="3fe769a7-5a01-4d2d-8fe0-e8d731b660f0" StartPoint="0.132200002670288,0.168400004506111" EndPoint="0.132200002670288,0.0844999998807907" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="#CBE7FF" />
<GradientStop Offset="1" Color="#EDFFFC" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Resources>
<Canvas>
<Path Fill="{StaticResource 3fe769a7-5a01-4d2d-8fe0-e8d731b660ed}" Data="M12.37 13a1 1 0 001.05-1v-.12c-.42-3.29-2.29-6-5.87-6S2 8.09 1.66 11.79A1.06 1.06 0 002.6 13h9.77z" />
<Path Fill="#FFFFFF" Data="M7.55 6.6a3.24 3.24 0 01-1.79-.53l1.77 4.62L9.29 6.1a3.24 3.24 0 01-1.74.5z" Opacity="0.8"/>
<Path Fill="{StaticResource 3fe769a7-5a01-4d2d-8fe0-e8d731b660ee}" Data="M7.55 6.6a3.3 3.3 0 100-6.6 3.3 3.3 0 000 6.6z" />
<Path Fill="{StaticResource 3fe769a7-5a01-4d2d-8fe0-e8d731b660ef}" Data="M16.02 7.57h-5.61a.32.32 0 00-.32.32v9.8c0 .177.143.32.32.32h5.61a.32.32 0 00.32-.32v-9.8a.32.32 0 00-.32-.32z" />
<Path Fill="#EFEFEF" Data="M13.4 17.19h-.36a.17.17 0 00-.17.17v.26c0 .094.076.17.17.17h.36a.17.17 0 00.17-.17v-.26a.17.17 0 00-.17-.17zm.45-9.27h-1.26a.09.09 0 00-.09.09v.01c0 .05.04.09.09.09h1.26c.05 0 .09-.04.09-.09v-.01a.09.09 0 00-.09-.09z" />
<Path Fill="{StaticResource 3fe769a7-5a01-4d2d-8fe0-e8d731b660f0}" Data="M15.64 8.45H10.8a.12.12 0 00-.12.12v8.15c0 .066.054.12.12.12h4.84a.12.12 0 00.12-.12V8.57a.12.12 0 00-.12-.12z" Opacity="0.9"/>
</Canvas>
</Canvas>
</Viewbox>
+11
View File
@@ -0,0 +1,11 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="52">
<Path Fill="#1D1D1D" Data="M28.125 46.875h-25A3.125 3.125 0 0 1 0 43.75V3.125A3.125 3.125 0 0 1 3.125 0h25a3.125 3.125 0 0 1 3.125 3.125V43.75a3.125 3.125 0 0 1-3.125 3.125z" />
<Path Fill="#5AB3D9" Data="M3.125 3.125h25v37.5h-25z" />
<Path Fill="#3898C6" Data="M28.125 40.625h-25V32.67l25-18.465z" />
<Path Data="M25 6.25V37.5H6.25V6.25H25m3.125-3.125h-25v37.5h25v-37.5z" Fill="#FFFFFF" />
<Path Fill="#E4E4E4" Data="M12.5 40.625h6.25v3.125H12.5z" />
<Path Fill="#7FBA42" Data="M37.5 15.625h6.25V50H37.5z" />
<Path Fill="#7FBA42" Data="m40.625 6.25-9.375 12.5H50z" />
</Canvas>
</Viewbox>
+8
View File
@@ -0,0 +1,8 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Fill="#5AB3D9" Data="M43.75 50H6.25A6.25 6.25 0 010 43.75V6.25A6.25 6.25 0 016.25 0h37.5A6.25 6.25 0 0150 6.25v37.5A6.25 6.25 0 0143.75 50z" />
<Path Fill="#3898C6" Data="M43.75 3.125a3.129 3.129 0 013.125 3.125v37.5a3.129 3.129 0 01-3.125 3.125H6.25a3.129 3.129 0 01-3.125-3.125V6.25A3.129 3.129 0 016.25 3.125h37.5m0-3.125H6.25A6.25 6.25 0 000 6.25v37.5A6.25 6.25 0 006.25 50h37.5A6.25 6.25 0 0050 43.75V6.25A6.25 6.25 0 0043.75 0z" />
<Path Fill="#FFFFFF" Data="M7.105 16.318l-.722-.776a.507.507 0 01.028-.722l2.101-1.928a.512.512 0 01.348-.137.51.51 0 01.381.163l5.77 6.134 9.922-12.611a.53.53 0 01.727-.086l2.265 1.739a.5.5 0 01.195.34.494.494 0 01-.105.375L15.266 25l-8.161-8.682zm33.604 11.807h-.084v6.334a3.041 3.041 0 01-3.041 3.041h-9.459v.084a3.041 3.041 0 003.041 3.041h9.544a3.041 3.041 0 003.041-3.041v-6.419a3.042 3.042 0 00-3.042-3.04z" />
<Path Fill="#FFFFFF" Data="M34.46 34.375h-9.544a3.04 3.04 0 01-3.04-3.04v-6.419a3.04 3.04 0 013.04-3.04h9.544a3.04 3.04 0 013.04 3.04v6.419a3.04 3.04 0 01-3.04 3.04z" />
</Canvas>
</Viewbox>
+18
View File
@@ -0,0 +1,18 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="18" Width="18">
<Canvas.Resources>
<LinearGradientBrush x:Key="a2546c3d-06f2-4d81-b3fa-b5b62d9f6f89" StartPoint="0.0900000035762787,0.170000001788139" EndPoint="0.0900000035762787,1" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#0078d4" />
<GradientStop Offset="0.16" Color="#1380da" />
<GradientStop Offset="0.53" Color="#3c91e5" />
<GradientStop Offset="0.82" Color="#559cec" />
<GradientStop Offset="1" Color="#5ea0ef" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Resources>
<Path Fill="{StaticResource a2546c3d-06f2-4d81-b3fa-b5b62d9f6f89}" Data="M6.6 12.2h4.8V17H6.6zM1 5.8h4.8V1H1.67a.67.67 0 00-.67.67zM1.67 17H5.8v-4.8H1v4.13a.67.67 0 00.67.67zM1 11.4h4.8V6.6H1zM12.2 17h4.13a.67.67 0 00.67-.67V12.2h-4.8zm-5.6-5.6h4.8V6.6H6.6zm5.6 0H17V6.6h-4.8zm0-10.4v4.8H17V1.67a.67.67 0 00-.67-.67zM6.6 5.8h4.8V1H6.6z" />
</Canvas>
</Viewbox>
+29
View File
@@ -0,0 +1,29 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="18" Width="18">
<Canvas.Resources>
<LinearGradientBrush x:Key="cf7d088c-fc7f-4c2c-8ffa-0b4b450e304a" StartPoint="0.0752099975943565,0.0372200012207031" EndPoint="0.0752099975943565,0.170129999518394" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#5ea0ef" />
<GradientStop Offset="1" Color="#0078d4" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="cf7d088c-fc7f-4c2c-8ffa-0b4b450e304b" StartPoint="0.143889993429184,0.0797099992632866" EndPoint="0.143889993429184,0.151940003037453" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#86d633" />
<GradientStop Offset="1" Color="#76bc2d" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Resources>
<Path Fill="{StaticResource cf7d088c-fc7f-4c2c-8ffa-0b4b450e304a}" Data="M12.9 12.721l-2.5-2.492a.731.731 0 01-.217-.522v-.563a.74.74 0 01.739-.739h4.116V3.113a.455.455 0 00-.478-.428H.479A.456.456 0 000 3.113v9.7a.455.455 0 00.479.428H12.9z" />
<Path Fill="#0078d4" Data="M.451 2.685h14.14a.429.429 0 01.45.4V4.7H0V3.088a.431.431 0 01.451-.403z" Opacity="0.7"/>
<Ellipse Canvas.Left="1.182" Canvas.Top="6.266" Width="1.228" Height="1.228" Fill="#FFFFFF" Opacity="0.7"/>
<Ellipse Canvas.Left="1.182" Canvas.Top="8.6269999999999989" Width="1.228" Height="1.228" Fill="#FFFFFF" Opacity="0.5"/>
<Path Data="M10.186 9.707v-.563a.721.721 0 01.115-.371H3.6a.267.267 0 00-.266.267v.408a.266.266 0 00.266.267h6.589z" Fill="#FFFFFF" Opacity="0.5"/>
<Rectangle Width="0.534" Height="0.942" Canvas.Left="3.333" Canvas.Top="6.416" Fill="#FFFFFF" Opacity="0.7"/>
<Path Fill="{StaticResource cf7d088c-fc7f-4c2c-8ffa-0b4b450e304b}" Data="M18 9.707v-.563A.148.148 0 0017.852 9h-6.927a.148.148 0 00-.148.148v.563a.149.149 0 00.044.1l2.622 2.621a.153.153 0 01.043.105v2.63a.148.148 0 00.148.148h1.51a.148.148 0 00.148-.148v-2.63a.148.148 0 01.043-.105l2.622-2.621A.153.153 0 0018 9.707z" />
</Canvas>
</Viewbox>
+13
View File
@@ -0,0 +1,13 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Data="M32 41c0 1.657-1.323 3-2.956 3H6.956C5.323 44 4 42.657 4 41V3c0-1.657 1.323-3 2.956-3h22.088C30.677 0 32 1.343 32 3v38z" Fill="#3e3e3e" />
<Path Data="M6 5h24v33H6z" Fill="#59b4d9" />
<Path Data="M23 2.817c0 .408-.53.738-1.186.738h-7.625c-.657 0-1.189-.33-1.189-.738s.532-.739 1.189-.739h7.625c.656 0 1.186.331 1.186.739" Fill="#ffffff" />
<Path Data="M26.021 0H7a3 3 0 00-3 3v38a3 3 0 003 3h2.6L26.021 0z" Fill="#ffffff" Opacity=".15" />
<Path Data="M32 11v7H18v-7z" Fill="#ffffff" />
<Path Data="M19 12h20v5H19z" Fill="#804998" />
<Path Data="M37 21.5v-14l10.5 6.462z" Fill="#804998" />
<Path Stroke="#3999C6" StrokeStartLineCap="round" StrokeEndLineCap="round" StrokeLineJoin="round" StrokeThickness="2" Data="M21.042 26.042h22.917v22.917H21.042z" Fill="#ffffff" />
<Path Fill="#3898C6" Data="M25 30h15v3H25zm0 6h12v3H25zm0 6h15v3H25z" />
</Canvas>
</Viewbox>
+5
View File
@@ -0,0 +1,5 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="16" Width="16">
<Path StrokeThickness="1.5" Fill="#707070" Data="M14.5 3h-1v11.5a1.3 1.3 0 0 1-.1.6l-.3.5-.5.3H3.4l-.5-.3-.3-.5a1.3 1.3 0 0 1-.1-.6V3h-1V2h4V1a.6.6 0 0 1 .1-.4l.2-.3.3-.2h3.8l.3.2.2.3a.6.6 0 0 1 .1.4v1h4zm-2 0h-9v11.5a.6.6 0 0 0 .1.4h8.8a.6.6 0 0 0 .1-.4zm-6 10h-1V5h1zm0-11h3V1h-3zm2 11h-1V5h1zm2 0h-1V5h1z" />
</Canvas>
</Viewbox>
+39
View File
@@ -0,0 +1,39 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="18" Width="18">
<Canvas.Resources>
<LinearGradientBrush x:Key="b04c7825-41db-4c22-aba2-f5c44b816ed3" StartPoint="0.0356499999761581,0.170000001788139" EndPoint="0.0356499999761581,0.0375300012528896" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#fea11b" />
<GradientStop Offset="0.127" Color="#feac19" />
<GradientStop Offset="0.562" Color="#ffcb12" />
<GradientStop Offset="0.804" Color="#ffd70f" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="e3016781-9cad-4dda-b769-212e01557f4d" StartPoint="0.0900000035762787,0.170000001788139" EndPoint="0.0900000035762787,0.0375300012528896" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#5e9624" />
<GradientStop Offset="0.546" Color="#6dad2a" />
<GradientStop Offset="0.999" Color="#76bc2d" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="a5967715-2146-4485-8c36-86a8b43a627b" StartPoint="0.144350007176399,0.170000001788139" EndPoint="0.144350007176399,0.0375300012528896" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#1b93eb" />
<GradientStop Offset="1" Color="#6bb9f2" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Resources>
<Rectangle Width="4.034" Height="13.247" Canvas.Left="1.367" Canvas.Top="3.753" Fill="{StaticResource b04c7825-41db-4c22-aba2-f5c44b816ed3}" RadiusX="0.517" RadiusY="0.517" />
<Rectangle Width="4.034" Height="13.247" Canvas.Left="6.802" Canvas.Top="3.753" Fill="{StaticResource e3016781-9cad-4dda-b769-212e01557f4d}" RadiusX="0.517" RadiusY="0.517" />
<Rectangle Width="4.034" Height="13.247" Canvas.Left="12.238" Canvas.Top="3.753" Fill="{StaticResource a5967715-2146-4485-8c36-86a8b43a627b}" RadiusX="0.517" RadiusY="0.517" />
<Path Fill="#faa21d" Data="M1.617 1h3.895a.251.251 0 01.251.251v2.871h-4.4V1.251A.251.251 0 011.617 1z" />
<Path Fill="#86d633" Data="M7.053 1h3.894a.251.251 0 01.251.251v2.871H6.8V1.251A.251.251 0 017.053 1z" />
<Path Fill="#5ea0ef" Data="M12.488 1h3.894a.251.251 0 01.251.251v2.871h-4.4V1.251A.251.251 0 0112.488 1z" />
</Canvas>
</Viewbox>
@@ -0,0 +1,7 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Data="M43.75 3.125H9.375v43.75h31.25v-37.5h3.125z" Fill="#FFFFFF" />
<Path Fill="#5AB2D7" Data="M34.375 12.5V9.375H12.5V12.5h21.875zm0 6.25v-3.125H12.5v3.125h21.875zm0 6.25v-3.125H12.5V25h21.875zm0 6.25v-3.125H12.5v3.125h21.875z" />
<Path Fill="#5AB2D7" Data="M43.75 0H15.626C9.376 0 6.25 3.125 6.25 9.375L6.251 37.5H6.25a6.25 6.25 0 100 12.5H37.5c3.393 0 6.25-2.857 6.25-6.25V9.375h5.383A6.185 6.185 0 0050 6.25 6.25 6.25 0 0043.75 0zm0 3.126c-.006.003-3.124 1.549-3.124 6.249V37.5h-.001v4.688a4.688 4.688 0 01-9.376 0c0-2.038 1.309-4.042 3.125-4.688H9.376L9.375 9.375c0-3.889 2.396-6.25 6.251-6.25l28.124.001z" />
</Canvas>
</Viewbox>
+7
View File
@@ -0,0 +1,7 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Fill="#57A300" Data="M38.185 5.96C27.169 4.745 25.326 0 25.326 0S22.847 6.242 7 6.242v25.611c0 3.097 1.721 5.997 4.104 8.532 5.402 5.748 14.223 9.616 14.223 9.616s18.327-8.029 18.327-18.149V6.242c-2.038 0-3.851-.103-5.469-.282z" />
<Path Fill="#B8D432" Data="M29.86 16.543L38.185 5.96C27.169 4.745 25.326 0 25.326 0S22.847 6.242 7 6.242v25.611c0 3.097 1.721 5.997 4.104 8.532l6.154-7.822 12.602-16.02z" Opacity=".4" />
<Path Fill="#FFFFFF" Data="M32.595 24.46h-1.066v-3.552a6.697 6.697 0 0 0-1.669-4.45c-.039-.043-.074-.09-.112-.133-1.107-1.186-2.683-1.948-4.422-1.947-1.736-.001-3.312.761-4.419 1.947a6.697 6.697 0 0 0-1.783 4.582v3.553h-1.065a.8.8 0 0 0-.801.801v9.392c0 .442.359.801.801.801h14.536a.801.801 0 0 0 .801-.801v-9.391a.801.801 0 0 0-.801-.802zm-4.011.001h-5.02l.001-.001H22.07v-3.552c0-1.022.386-1.927.988-2.57.605-.643 1.395-1.013 2.268-1.013.874 0 1.665.37 2.27 1.013.143.153.271.323.389.504l-.001.001c.373.579.6 1.288.6 2.064v3.554z" />
</Canvas>
</Viewbox>
+20
View File
@@ -0,0 +1,20 @@
<Viewbox Width="18" Height="18" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Viewbox.Resources>
<LinearGradientBrush x:Key="82ede37b-be1f-4ec6-a8d4-aefcb532a259" StartPoint="0,11.9927997589111" EndPoint="0,11.9345598220825" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#5ea0ef" />
<GradientStop Offset="1" Color="#0078d4" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Viewbox.Resources>
<Canvas Height="18" Width="18">
<Path Fill="{StaticResource 82ede37b-be1f-4ec6-a8d4-aefcb532a259}" Data="M0 7.146L.011 10.5a.578.578 0 00.176.43l3.238 2.8a.77.77 0 00.409.169l13.581.039a.588.588 0 00.585-.59l-.014-8.635a.6.6 0 00-.591-.592l-13.61-.053a.89.89 0 00-.38.141L.205 6.7A.569.569 0 000 7.146zm3.134.925a1.076 1.076 0 11-1.522 0l.007-.007a1.067 1.067 0 011.508 0z" />
<Path Fill="#005ba1" Data="M1.279 9.921a1.554 1.554 0 100-2.2 1.554 1.554 0 000 2.2zM1.893 8.1a.874.874 0 11-.241.241.891.891 0 01.241-.241z" />
<Path Data="M15.762 6.978h-5.286a.522.522 0 100 1.044h5.338a.522.522 0 100-1.044z" Fill="#ffffff" Opacity="0.6" />
<Path Data="M6.74 8.787a.245.245 0 00.347 0l2.1-2.1a.245.245 0 000-.347l-.163-.163a.246.246 0 00-.346 0L6.914 7.936l-.607-.606a.249.249 0 00-.346 0L5.8 7.5a.246.246 0 000 .346z" Fill="#ffffff" Opacity="0.8" />
<Path Data="M15.788 10.016H10.5a.522.522 0 000 1.044h5.338a.522.522 0 100-1.044z" Fill="#ffffff" Opacity="0.6" />
<Path Data="M6.767 11.825a.244.244 0 00.346 0l2.1-2.1a.247.247 0 000-.347l-.164-.163a.245.245 0 00-.346 0L6.94 10.974l-.606-.607a.251.251 0 00-.346 0l-.166.166a.245.245 0 000 .347z" Fill="#ffffff" Opacity="0.8" />
</Canvas>
</Viewbox>
@@ -0,0 +1,9 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Fill="#3898C6" Data="M0 44.963c0 1.056.898 1.912 2.007 1.912h45.986c1.109 0 2.007-.857 2.007-1.912v-33.61H0v33.61z" />
<Path Fill="#7B7B7B" Data="M47.993 3.125H2.007A2.014 2.014 0 000 5.149V12.5h50V5.149a2.015 2.015 0 00-2.007-2.024z" />
<Path Data="M3.125 12.5h43.75v31.25H3.125z" Fill="#FFFFFF" />
<Path Fill="#3898C6" Data="M25 18.75h15.625v3.125H25zM25 25h12.5v3.125H25zm0 6.25h15.625v3.125H25zM9.375 18.75V37.5h12.5V18.75h-12.5zm9.375 15.625H12.5v-12.5h6.25v12.5z" />
<Path Fill="#FFFFFF" Data="M43.75 6.25h3.125v3.125H43.75zm-6.25 0h3.125v3.125H37.5zm-6.25 0h3.125v3.125H31.25z" />
</Canvas>
</Viewbox>
+38
View File
@@ -0,0 +1,38 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="18" Width="18">
<Canvas.Resources>
<LinearGradientBrush x:Key="aea576e5-fef8-4bf5-a6dd-590c075a08c5" StartPoint="0.0834000036120415,0.119599997997284" EndPoint="0.0834000036120415,0.0186000000685453" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#d2ebff" />
<GradientStop Offset="1" Color="#f0fffd" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ae1611cd-4cf1-4ed1-b5e1-db19220ee586" StartPoint="0.141499996185303,0.204099997878075" EndPoint="0.141499996185303,0.0233999993652105" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#0078d4" />
<GradientStop Offset="0.17" Color="#1c84dc" />
<GradientStop Offset="0.38" Color="#3990e4" />
<GradientStop Offset="0.59" Color="#4d99ea" />
<GradientStop Offset="0.8" Color="#5a9eee" />
<GradientStop Offset="1" Color="#5ea0ef" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="bb0a651b-bb97-4569-a23e-5fed56c1e359" StartPoint="0.141499996185303,0.155300006270409" EndPoint="0.141499996185303,0.0520000010728836" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Resources>
<Rectangle Width="16.68" Height="11.87" Canvas.Top="1.03" Fill="#0078d4" RadiusX="0.6" RadiusY="0.6" />
<Rectangle Width="14.74" Height="10.1" Canvas.Left="0.97" Canvas.Top="1.86" Fill="{StaticResource aea576e5-fef8-4bf5-a6dd-590c075a08c5}" Opacity='0.9' RadiusX="0.3" RadiusY="0.3" />
<Rectangle Width="7.7" Height="12.85" Canvas.Left="10.3" Canvas.Top="4.12" Fill="{StaticResource ae1611cd-4cf1-4ed1-b5e1-db19220ee586}" RadiusX="0.3" RadiusY="0.3" />
<Rectangle Width="1.77" Height="0.24" Canvas.Left="13.27" Canvas.Top="4.55" Fill="#f2f2f2" RadiusX="0.11" RadiusY="0.11" />
<Rectangle Width="6.25" Height="10.33" Canvas.Left="11.03" Canvas.Top="5.2" Fill="{StaticResource bb0a651b-bb97-4569-a23e-5fed56c1e359}" Opacity='0.9' RadiusX="0.14" RadiusY="0.14" />
<Rectangle Width="1.77" Height="0.24" Canvas.Left="7.48" Canvas.Top="1.4" Fill="#f2f2f2" RadiusX="0.11" RadiusY="0.11" />
<Rectangle Width=".86" Height="0.73" Canvas.Left="13.72" Canvas.Top="15.96" Fill="#f2f2f2" RadiusX="0.2" RadiusY="0.2" />
</Canvas>
</Viewbox>
+19
View File
@@ -0,0 +1,19 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="18" Width="18">
<Canvas.Resources>
<LinearGradientBrush x:Key="A" StartPoint="0.0703999996185303,0.75" EndPoint="0.0703999996185303,0.163499996066093" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0.18" Color="#5ea0ef" />
<GradientStop Offset="1" Color="#0078d4" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Resources>
<Path Data="M14 8c0 4.58-5.53 8.26-6.73 9a.47.47 0 0 1-.46 0C5.6 16.25.07 12.57.07 8V2.48a.44.44 0 0 1 .43-.43C4.8 1.93 3.8 0 7 0s2.23 1.9 6.54 2a.44.44 0 0 1 .42.43z" Fill="#5ea0ef" />
<Path Data="M13.42 8c0 4.2-5.08 7.57-6.18 8.26a.4.4 0 0 1-.42 0C5.72 15.6.65 12.23.65 8V3A.39.39 0 0 1 1 2.59C5 2.48 4.07.75 7 .75s2.05 1.73 6 1.84a.4.4 0 0 1 .4.4z" Fill="#60b0ef" />
<Path Data="M7 8.55V.75c3 0 2.05 1.73 6 1.84a.41.41 0 0 1 .4.4V8.5zm0 0H.67c.4 4 5.1 7.1 6.15 7.75a.34.34 0 0 0 .17.05H7z" Fill="{StaticResource A}" />
<Path Data="M1 2.6C5 2.48 4.07.75 7 .75v7.8H.67V3A.4.4 0 0 1 1 2.59zm12.4 5.95H7v7.8a.34.34 0 0 0 .17-.05c1.13-.66 5.83-3.8 6.22-7.75z" Fill="#0078d4" />
<Path Data="M14.13 17.8l.33-1.08.63-.37 1.1.48.7-.72V16l-.5-1 .3-.68 1.1-.4.12-.05v-1h-.14l-1.08-.33-.36-.63.47-1.1-.72-.7H16l-1 .5-.7-.3-.44-1.22h-1l-.05.14-.33 1.08-.7.3-1.18-.54-.7.72.07.13.52 1-.28.7-1.2.45v1l.14.05 1.08.33.3.7L10 16.3l.73.7.13-.07 1-.53.7.28.52 1.3h1zm-1.63-3.23a1.45 1.45 0 1 1 2 0 1.44 1.44 0 0 1-2 0z" Fill="#949494" />
</Canvas>
</Viewbox>
@@ -0,0 +1,15 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Data="M46 33c0 1.657-1.359 3-3.036 3H3.036C1.359 36 0 34.657 0 33V3c0-1.657 1.359-3 3.036-3h39.928C44.641 0 46 1.343 46 3v30z" Fill="#3E3E3E" />
<Path Fill="#3898C6" Data="M2.962 5H43v26H2.962z" />
<Path Data="M31 0H3a3 3 0 00-3 3v30a3 3 0 003 3h12L31 0z" Fill="#ffffff" Opacity=".15" />
<Path Data="M26 2.689c0 .38-.308.688-.688.688H20.89a.689.689 0 110-1.377h4.422c.38 0 .688.309.688.689" Fill="#ffffff" />
<Path Data="M50 47.545C50 48.901 48.913 50 47.572 50H29.428C28.087 50 27 48.901 27 47.545v-31.09C27 15.099 28.087 14 29.428 14h18.143C48.913 14 50 15.099 50 16.455v31.09z" Fill="#3E3E3E" />
<Path Fill="#5AB3D9" Data="M29 18h19v27H29z" />
<Path Data="M42.607 16.171c0 .335-.436.606-.974.606H35.37c-.54 0-.977-.271-.977-.606 0-.335.437-.607.977-.607h6.263c.538 0 .974.272.974.607" Fill="#ffffff" />
<Path Data="M45.089 14H29.464A2.46 2.46 0 0027 16.455v31.091A2.458 2.458 0 0029.464 50H31.6l13.489-36z" Opacity=".15" Fill="#ffffff" />
<Ellipse Canvas.Left="8" Canvas.Top="20" Width="27" Height="27" Fill="#B82025" />
<Path Fill="#2A3280" Data="M21.5 21C28.393 21 34 26.607 34 33.5S28.393 46 21.5 46 9 40.393 9 33.5 14.607 21 21.5 21m0-1C14.044 20 8 26.044 8 33.5S14.044 47 21.5 47 35 40.956 35 33.5 28.956 20 21.5 20z" Opacity=".5" />
<Path Data="M22.856 36l.679-11H19.46l.679 11h2.717zm-1.354 2.1a1.903 1.903 0 00-1.905 1.899c0 1.05.854 1.901 1.905 1.901a1.902 1.902 0 001.904-1.901 1.902 1.902 0 00-1.904-1.899z" Fill="#ffffff" />
</Canvas>
</Viewbox>
@@ -0,0 +1,11 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Data="M0 44.062C0 45.132.898 46 2.007 46h45.986C49.102 46 50 45.132 50 44.062V11H0v33.062z" Fill="#3999C6" />
<Path Data="M47.993 3H2.007C.898 3 0 3.966 0 5.159V12h50V5.159C50 3.966 49.101 3 47.993 3z" Fill="#7a7a7a" />
<Path Data="M2.014 3C.906 3 .007 3.861.007 4.923v39.153C.007 45.138.906 46 2.014 46h17.188L41.62 3H2.014z" Fill="#ffffff" Opacity=".1"/>
<Path Data="M3 12h44v31H3z" Fill="#ffffff" />
<Path Data="M25 19h16v3H25zm0 12h16v3H25z" Fill="#3999C6" />
<Path Data="M44 6h3v3h-3zm-6 0h3v3h-3zm-6 0h3v3h-3z" Fill="#ffffff" />
<Path Data="M7.161 32.753a.463.463 0 01-.123-.334.46.46 0 01.151-.315l1.21-1.119a.456.456 0 01.31-.122c.129 0 .245.052.334.149l3.208 3.44 5.622-7.202a.448.448 0 01.36-.176c.103 0 .199.034.28.098l1.31 1.011a.43.43 0 01.176.302c.01.1-.002.236-.108.346l-7.233 9.262a.33.33 0 01-.501.022l-4.996-5.362zm0-12.072a.463.463 0 01-.123-.334.46.46 0 01.151-.315l1.21-1.119a.456.456 0 01.31-.122c.129 0 .245.052.334.149l3.208 3.44 5.622-7.202a.448.448 0 01.36-.176c.103 0 .199.034.28.098l1.31 1.011a.43.43 0 01.176.302c.01.1-.002.236-.108.346l-7.233 9.262a.33.33 0 01-.501.022l-4.996-5.362z" Fill="#3999C6" />
</Canvas>
</Viewbox>
+10
View File
@@ -0,0 +1,10 @@
<Viewbox Width="18" Height="18" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Width="18" Height="18">
<Path Fill="#FF225086" Data="m3.802,14.032c.388,.242,1.033,.511,1.715,.511 .621,0,1.198,-.18,1.676,-.487 0,0,.001,0,.002,-.001 l1.805,-1.128 v4.073 c-.286,0,-.574,-.078,-.824,-.234 l-4.374,-2.734 Z"/>
<Path Fill="#FF66DDFF" Data="m7.853,1.507 L.353,9.967 c-.579,.654,-.428,1.642,.323,2.111 0,0,2.776,1.735,3.126,1.954 .388,.242,1.033,.511,1.715,.511 .621,0,1.198,-.18,1.676,-.487 0,0,.001,0,.002,-.001 l1.805,-1.128 -4.364,-2.728 4.365,-4.924 V1 s0,0,0,0 c-.424,0,-.847,.169,-1.147,.507 Z"/>
<Polygon Fill="#FFcbf8ff" Points="4.636,10.199 4.688,10.231 9,12.927 9.001,12.927 9.001,5.276 9,5.275 4.636,10.199"/>
<Path Fill="#FF074793" Data="m17.324,12.078 c.751,-.469,.902,-1.457,.323,-2.111 l-4.921,-5.551 c-.397,-.185,-.842,-.291,-1.313,-.291 -.925,0,-1.752,.399,-2.302,1.026 l-.109,.123 h0 s4.364,4.924,4.364,4.924 h0 s0,0,0,0 l-4.365,2.728 v4.073 c.287,0,.573,-.078,.823,-.234 l7.5,-4.688 Z"/>
<Path Fill="#FF0294e4" Data="m9.001,1 v4.275 s.109,-.123,.109,-.123 c.55,-.627,1.377,-1.026,2.302,-1.026 .472,0,.916,.107,1.313,.291 l-2.579,-2.909 c-.299,-.338,-.723,-.507,-1.146,-.507 Z"/>
<Polygon Fill="#FF96bcc2" Points="13.365,10.199 9.001,5.276 9.001,12.926 13.365,10.199"/>
</Canvas>
</Viewbox>
+5
View File
@@ -0,0 +1,5 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Fill="#2072B8" Data="M25 4.885v16.99h21.875V3.124zM3.125 6.647v15.228h18.75V5.137zM25 41.989v-16.99h21.875V43.75zM3.125 40.227V24.999h18.75v16.738z" />
</Canvas>
</Viewbox>
+26
View File
@@ -0,0 +1,26 @@
<Viewbox Width="18" Height="18" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Viewbox.Resources>
<LinearGradientBrush x:Key="blueBase" MappingMode="Absolute" StartPoint="171.5,396.0" EndPoint="314.3,90.6">
<GradientStop Offset="0.02" Color="#FF1169DA"/>
<GradientStop Offset="0.44" Color="#FF0151BD"/>
<GradientStop Offset="0.61" Color="#FF014DB7"/>
<GradientStop Offset="1" Color="#FF126AD9"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="cyanWave" MappingMode="Absolute" StartPoint="114.7,97.5" EndPoint="182.1,383.7">
<GradientStop Offset="0" Color="#FF92EEFE"/>
<GradientStop Offset="0.56" Color="#FF35DDFF"/>
<GradientStop Offset="1" Color="#FF08B1F9"/>
</LinearGradientBrush>
</Viewbox.Resources>
<Canvas Width="401" Height="396" ClipToBounds="True">
<!-- Outer wave shape (dark blue) -->
<Path Fill="{StaticResource blueBase}"
Data="M0.685 65.852 C0.685 14.765 67.486 -13.203 111.616 19.408 L207.551 90.254 L288.514 44.373 C331.305 21.039 399.874 37.775 400.685 101.559 L400.685 336.622 C400.685 387.71 335.845 414.343 291.715 381.732 L194.031 309.34 L112.973 355.23 C69.778 378.784 0.753 360.714 0.753 298.637 C0.753 224.124 0.685 252.734 0.685 65.852 Z"/>
<!-- Inner face (medium cyan — creates the 3-D fold effect) -->
<Path Fill="#FF23C0FE"
Data="M111.616 19.408 C67.486 -13.203 0.685 14.765 0.685 65.852 V295.742 C0.685 290.153 3.533 279.026 3.533 279.026 C16.695 236.001 72.649 219.869 111.36 248.223 L291.715 381.732 C335.845 414.343 400.685 387.71 400.685 336.622 V101.559 C400.685 145.201 343.643 189.011 289.755 151.051 Z"/>
<!-- Foreground wave highlight (light cyan gradient) -->
<Path Fill="{StaticResource cyanWave}"
Data="M111.616 149.691 C67.486 117.08 0.685 145.048 0.685 196.135 L0.685 65.852 C0.685 14.765 67.486 -13.203 111.616 19.408 C170.996 63.289 230.374 107.171 289.755 151.051 C343.643 189.011 400.685 145.201 400.685 101.559 V234.89 C400.685 285.978 333.885 313.946 289.755 281.334 C230.374 237.454 170.996 193.572 111.616 149.691 Z"/>
</Canvas>
</Viewbox>
+11
View File
@@ -0,0 +1,11 @@
<Viewbox Width="18" Height="18" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="18" Width="18">
<Path Data="M0 0h16v16H0z" Fill="#FFFFFF" />
<Path Data="M0 0h16v16H0z" Fill="#FFFFFF" />
<Path Data="M15.429 13.429H.57L0 12.857V2.571L.571 2H15.43l.571.571v10.286l-.571.572ZM1.143 12.286h13.714V3.143H1.143v9.143Z" Fill="#212121" />
<Path Data="M14.859 3.143H1.145v9.143h13.714V3.143Z" Fill="#FEF7B2" />
<Path Data="M14.859 12.286H1.145V10l13.714-4.571v6.857Z" Fill="#FDE300" />
<Path Data="M13.716 4.286v6.857H2.287V4.286h11.429Zm1.143-1.143H1.145v9.143h13.714V3.143Z" Fill="#FFFFFF" />
<Path Data="M10.286 12.286H5.715v1.143h4.571v-1.143Z" Fill="#9E9FA0" />
</Canvas>
</Viewbox>
+5
View File
@@ -0,0 +1,5 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="16" Width="16">
<Path Fill="#000000" Data="M8 0a7.6 7.6 0 012.1.3 6.4 6.4 0 011.9.8 10.4 10.4 0 011.7 1.2A10.4 10.4 0 0114.9 4a6.4 6.4 0 01.8 1.9 7.5 7.5 0 010 4.2 6.4 6.4 0 01-.8 1.9 10.4 10.4 0 01-1.2 1.7 10.4 10.4 0 01-1.7 1.2 6.4 6.4 0 01-1.9.8 7.5 7.5 0 01-4.2 0 6.4 6.4 0 01-1.9-.8 10.4 10.4 0 01-1.7-1.2A10.4 10.4 0 011.1 12a6.4 6.4 0 01-.8-1.9 7.5 7.5 0 010-4.2A6.4 6.4 0 011.1 4a10.4 10.4 0 011.2-1.7A10.4 10.4 0 014 1.1 6.4 6.4 0 015.9.3 7.6 7.6 0 018 0zM1 8a6.4 6.4 0 00.3 2h2.8A3.4 3.4 0 004 9V7a3.4 3.4 0 00.1-1H1.3A6.4 6.4 0 001 8zm4.7-6.6a3.4 3.4 0 00-1.2.6l-1.2.8-.9 1A7.5 7.5 0 001.7 5h2.6a2.9 2.9 0 01.2-.9l.3-1 .4-.9a5.6 5.6 0 01.5-.8zm-4 9.6a7.5 7.5 0 00.7 1.2l.9 1 1.2.8a3.4 3.4 0 001.2.6 5.6 5.6 0 01-.5-.8l-.4-.9-.3-1a2.9 2.9 0 01-.2-.9zm9.2-1a3.4 3.4 0 01.1-1V7a3.4 3.4 0 01-.1-1H5.1A3.4 3.4 0 015 7v2a3.4 3.4 0 01.1 1zM8 1l-.7.2-.6.6-.5.7a3.6 3.6 0 00-.4.9l-.3.9a2.5 2.5 0 00-.2.7h5.4a2.5 2.5 0 00-.2-.7l-.3-.9a3.6 3.6 0 00-.4-.9l-.5-.7-.6-.6zm0 14l.7-.2.6-.6.5-.7a3.6 3.6 0 00.4-.9l.3-.9a2.5 2.5 0 00.2-.7H5.3a2.5 2.5 0 00.2.7l.3.9a3.6 3.6 0 00.4.9l.5.7.6.6zm6.3-10a7.5 7.5 0 00-.7-1.2l-.9-1-1.2-.8-1.2-.6a5.6 5.6 0 01.5.8l.4.9.3 1a2.9 2.9 0 01.2.9zm-4 9.6a3.4 3.4 0 001.2-.6l1.2-.8.9-1a7.5 7.5 0 00.7-1.2h-2.6a2.9 2.9 0 01-.2.9l-.3 1-.4.9a5.6 5.6 0 01-.5.8zM15 8a6.4 6.4 0 00-.3-2h-2.8a3.4 3.4 0 00.1 1v2a3.4 3.4 0 00-.1 1h2.8a6.4 6.4 0 00.3-2z" />
</Canvas>
</Viewbox>
+16
View File
@@ -0,0 +1,16 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="64" Width="64">
<Ellipse Canvas.Left="1.75" Canvas.Top="1.75" Width="60.5" Height="60.5" StrokeThickness="1.5" Stroke="#707070" />
<Canvas Height="64" Width="64">
<Canvas.RenderTransform>
<MatrixTransform Matrix=".9 0 0 .9 10.4 10.4" />
</Canvas.RenderTransform>
<Rectangle Canvas.Left="13.3" Canvas.Top="12.3" Width="21.4" Height="28.5" StrokeThickness="2.5" Stroke="#707070" RadiusX="0.6" RadiusY="0.6" />
<Ellipse Canvas.Top="21.799999999999997" Canvas.Left="20.4" Width="7.2" Height="7.2" StrokeThickness="2.5" Stroke="#707070" />
<Path Data="M18 35a1 1 0 1112 0" StrokeThickness="2.5" Stroke="#707070" />
</Canvas>
<Canvas>
<Path Data="M36.68 14h2.34l-3.24 6.75h-2.43zM24.89 14h2.43l5.58 11.25a1.046 1.046 0 01-1.791 1.08l-.549-1.08z" Fill="#707070" />
</Canvas>
</Canvas>
</Viewbox>
+12
View File
@@ -0,0 +1,12 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="64" Width="64">
<Ellipse Canvas.Left="1.75" Canvas.Top="1.75" Width="60.5" Height="60.5" StrokeThickness="1.5" Stroke="#707070" />
<Canvas Height="64" Width="64">
<Canvas.RenderTransform>
<MatrixTransform Matrix="1.1 0 0 1.1 8.8 5.61" />
</Canvas.RenderTransform>
<Ellipse Canvas.Top="10.5" Canvas.Left="13" Width="14" Height="14" StrokeThickness="2.5" Stroke="#707070" />
<Path Data="M30 35h10m-5-5v10M30.833 32.09A11 11 0 009 34" StrokeThickness="2.5" Stroke="#707070" />
</Canvas>
</Canvas>
</Viewbox>
+22
View File
@@ -0,0 +1,22 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Viewbox.Resources>
<LinearGradientBrush x:Key="2234a2b0-8e35-4cb8-93a2-033d950b05a2" StartPoint="0.090379998087883,0.101590000092983" EndPoint="0.090379998087883,0.0783400014042854" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#5e9624" />
<GradientStop Offset="0.241" Color="#6fb02a" />
<GradientStop Offset="0.501" Color="#7cc52f" />
<GradientStop Offset="0.756" Color="#83d232" />
<GradientStop Offset="1" Color="#86d633" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Viewbox.Resources>
<Canvas Height="18" Width="18">
<Path Fill="{StaticResource 2234a2b0-8e35-4cb8-93a2-033d950b05a2}" Data="M13.9 8.989a1.158 1.158 0 11-1.159-1.155A1.157 1.157 0 0113.9 8.989zm-4.866-1.15a1.158 1.158 0 101.159 1.156 1.157 1.157 0 00-1.159-1.156zm-3.7 0A1.158 1.158 0 106.493 9a1.158 1.158 0 00-1.159-1.156z" />
<Path Fill="#50e6ff" Data="M6.184 13.633l-.664.665a.3.3 0 01-.424 0L.184 9.402a.6.6 0 01-.001-.848l.664-.666 5.335 5.32a.3.3 0 010 .425z" />
<Path Fill="#1490df" Data="M5.421 3.701l.67.667a.3.3 0 010 .425l-5.245 5.26-.666-.664a.6.6 0 010-.849L5 3.706a.3.3 0 01.425 0z" />
<Path Fill="#50e6ff" Data="M17.154 7.885l.664.665a.6.6 0 01-.001.849l-4.912 4.897a.3.3 0 01-.424 0l-.664-.666a.3.3 0 010-.424l5.337-5.321z" />
<Path Fill="#1490df" Data="M17.823 9.386l-.665.664-5.245-5.26a.3.3 0 010-.424l.67-.668a.3.3 0 01.424 0l4.82 4.834a.6.6 0 01-.002.849z" />
</Canvas>
</Viewbox>
+8
View File
@@ -0,0 +1,8 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="20.9" Width="19.9">
<Path Data="M8 17.1c-.2 0-.4.3-.4.8 0 1.5 1.2 2.6 2.7 2.6S13 19.4 13 18c0-.5-.1-.9-.4-.9H8z" Fill="#FCD116" />
<Path Data="M17 16.1V6.7c0-3.4-3.4-6.2-7-6.2S3 3.3 3 6.7v9.4H1v2h18v-2h-2z" Fill="#FF8C00" />
<Path Data="M14.6 2.3C13.4 1.2 11.6.5 9.7.5 6.1.5 3 3.3 3 6.7v7L14.6 2.3z" Opacity="0.2" Fill="#FFFFFF" />
<Path Data="M1 16.1h18v2H1z" Fill="#1e1e1e" Opacity="0.2" />
</Canvas>
</Viewbox>
+24
View File
@@ -0,0 +1,24 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="18" Width="18">
<Canvas.Resources>
<LinearGradientBrush x:Key="de1cccc6-47f0-46c5-9f1a-e61eeb0d70ee" StartPoint="0.0966000035405159,0.152300000190735" EndPoint="0.0966000035405159,0.0500999987125397" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#0078d4" />
<GradientStop Offset="0.82" Color="#5ea0ef" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Resources>
<Path Data="M4.37 3.58h7.97v9.68H4.37z" Fill="#FFFFFF" />
<Path Fill="#0078d4" Data="M5.08 13h-.46V3.83h7.48v3.94h.5V3.58a.25.25 0 00-.25-.25h-8a.28.28 0 00-.25.25v9.69a.27.27 0 00.08.18.26.26 0 00.17.07h.71z" />
<Path Data="M9.84 4.61H5.42a.35.35 0 00-.35.39v9.38a.35.35 0 00.35.34h7.68a.35.35 0 00.34-.35V8.12a.35.35 0 00-.35-.35h-2.54a.35.35 0 01-.35-.35V5a.34.34 0 00-.36-.39z" Fill="#FFFFFF" />
<Path Fill="{StaticResource de1cccc6-47f0-46c5-9f1a-e61eeb0d70ee}" Data="M9.68 5.05V7.4a.86.86 0 00.87.86H13v6H5.55V5.05h4.13m.17-.52H5.37a.36.36 0 00-.37.35v9.52a.36.36 0 00.36.35h7.77a.35.35 0 00.36-.35V8.09a.34.34 0 00-.36-.34h-2.57a.35.35 0 01-.35-.35V4.88a.36.36 0 00-.36-.35z" />
<Path Fill="#0078d4" Data="M13.36 7.81l-3.28-3.2v2.61a.59.59 0 00.59.6z" />
<Path Fill="#4d4d4d" Data="M11.72 9.34h-5a.17.17 0 01-.18-.17.18.18 0 01.22-.17h5a.18.18 0 01.18.18.17.17 0 01-.22.16zm0 1h-5a.18.18 0 01-.18-.18.18.18 0 01.22-.16h5a.18.18 0 01.18.17.18.18 0 01-.22.17zm0 1.01h-5a.18.18 0 01-.18-.18.17.17 0 01.22-.17h5a.17.17 0 01.18.17.18.18 0 01-.22.18z" />
<Path Fill="#949494" Data="M.5 15.39a.15.15 0 00.09.14l1.23.69 2.1 1.17a.17.17 0 00.23-.06l.71-1.18a.17.17 0 00-.06-.23l-2.46-1.37a.16.16 0 01-.08-.14V3.59a.16.16 0 01.08-.14L4.8 2.08a.17.17 0 00.06-.23L4.15.67a.17.17 0 00-.23-.06L1.86 1.76l-1.27.71a.15.15 0 00-.09.14v12.78z" />
<Path Fill="#a3a3a3" Data="M2.25 3.51a.13.13 0 01.07-.06l2.45-1.37a.17.17 0 00.06-.23L4.13.67a.19.19 0 00-.24-.06l-2 1.15-1.33.71a.17.17 0 00-.06.06l1 .53zm2.52 12.41l-2.45-1.37a.2.2 0 01-.08-.08l-1.73 1h.05l1.24.69 2.09 1.17a.19.19 0 00.24-.06l.7-1.18a.17.17 0 00-.06-.17z" />
<Path Fill="#949494" Data="M17.5 15.39a.15.15 0 01-.09.14l-1.23.69-2.1 1.17a.17.17 0 01-.23-.06l-.71-1.18a.17.17 0 01.06-.23l2.46-1.37a.16.16 0 00.08-.14V3.59a.16.16 0 00-.08-.14L13.2 2.08a.17.17 0 01-.06-.23l.71-1.18a.17.17 0 01.23-.06l2.06 1.15 1.27.71a.15.15 0 01.09.14v12.78z" />
<Path Fill="#a3a3a3" Data="M15.75 3.51a.13.13 0 00-.07-.06l-2.45-1.37a.17.17 0 01-.06-.23l.7-1.18a.19.19 0 01.24-.06l2.05 1.15 1.28.71a.17.17 0 01.06.06l-1 .53zm-2.52 12.41l2.45-1.37a.2.2 0 00.08-.08l1.73 1-1.24.69-2.09 1.17a.19.19 0 01-.24-.06l-.7-1.18a.17.17 0 01.01-.17z" />
</Canvas>
</Viewbox>
+5
View File
@@ -0,0 +1,5 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="16" Width="16">
<Path Fill="#000000" Data="M12.5 1.4a10.7 10.7 0 011.9 1.8 7.8 7.8 0 011.2 2.2A7.5 7.5 0 0116 8a7.6 7.6 0 01-.3 2.1 6.4 6.4 0 01-.8 1.9 10.4 10.4 0 01-1.2 1.7 10.4 10.4 0 01-1.7 1.2 6.4 6.4 0 01-1.9.8 7.5 7.5 0 01-4.2 0 6.4 6.4 0 01-1.9-.8 10.4 10.4 0 01-1.7-1.2A10.4 10.4 0 011.1 12a6.4 6.4 0 01-.8-1.9 7.5 7.5 0 010-4.2 11.9 11.9 0 01.8-2 10.1 10.1 0 011.3-1.6A10.9 10.9 0 014.1 1H2V0h4v4H5V1.7a6.2 6.2 0 00-1.7 1.1 6.1 6.1 0 00-1.2 1.5A5.3 5.3 0 001.3 6 6.9 6.9 0 001 8a6.3 6.3 0 00.3 1.9 4.6 4.6 0 00.7 1.6 4.9 4.9 0 001.1 1.4A4.9 4.9 0 004.5 14l1.6.8L8 15l1.9-.2 1.6-.8a4.9 4.9 0 001.4-1.1 4.9 4.9 0 001.1-1.4 8 8 0 00.8-1.6A12.3 12.3 0 0015 8a6.2 6.2 0 00-.4-2.3 6.9 6.9 0 00-1-1.9 8.7 8.7 0 00-1.7-1.6 7.2 7.2 0 00-2-.9l.2-1a6.6 6.6 0 012.4 1.1z" />
</Canvas>
</Viewbox>
+7
View File
@@ -0,0 +1,7 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="50" Width="50">
<Path Fill="#3898C6" Data="M9.375 3.125h37.5V50h-37.5z" />
<Path Fill="#7B7B7B" Data="M3.125 6.25H12.5v3.125H3.125zm0 9.375H12.5v3.125H3.125zm0 9.375H12.5v3.125H3.125zm0 9.375H12.5V37.5H3.125zm0 9.375H12.5v3.125H3.125z" />
<Path Data="M18.75 15.625H37.5V25H18.75z" Fill="#ffffff" />
</Canvas>
</Viewbox>
+30
View File
@@ -0,0 +1,30 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="18" Width="18">
<Canvas.Resources>
<LinearGradientBrush x:Key="de1cccc6-47f0-46c5-9f1a-e61eeb0d7154" StartPoint="0.0793000012636185,0.179499998688698" EndPoint="0.0793000012636185,0.0562000013887882" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#5e9624" />
<GradientStop Offset="1" Color="#b4ec36" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="de1cccc6-47f0-46c5-9f1a-e61eeb0d7155" StartPoint="0.0794999971985817,0.0921000018715858" EndPoint="0.0794999971985817,0" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#5e9624" />
<GradientStop Offset="1" Color="#b4ec36" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Resources>
<Path Fill="{StaticResource de1cccc6-47f0-46c5-9f1a-e61eeb0d7154}" Data="M14 16.85a1.3 1.3 0 001.32-1.31.81.81 0 000-.16c-.52-4.15-2.88-7.53-7.4-7.53S1 10.71.51 15.39a1.34 1.34 0 001.19 1.46H14z" />
<Path Data="M8 8.83a4.16 4.16 0 01-2.26-.66L7.92 14l2.22-5.79A4.2 4.2 0 018 8.83z" Fill="#ffffff" />
<Ellipse Canvas.Left="3.7800000000000002" Canvas.Top="0.5" Width="8.34" Height="8.34" Fill="{StaticResource de1cccc6-47f0-46c5-9f1a-e61eeb0d7155}" />
<Path Fill="#32bedd" Data="M17.49 11.13v4.24l-3.64 2.13v-4.24l3.64-2.13z" />
<Path Fill="#9cebff" Data="M17.49 11.13l-3.64 2.13-3.64-2.13L13.85 9l3.64 2.13z" />
<Path Fill="#50e6ff" Data="M13.85 13.26v4.24l-3.64-2.13v-4.24l3.64 2.13z" />
<Path Fill="#9cebff" Data="M10.21 15.37l3.64-2.11v4.24l-3.64-2.13z" />
<Path Fill="#50e6ff" Data="M17.49 15.37l-3.64-2.11v4.24l3.64-2.13z" />
</Canvas>
</Viewbox>
+7
View File
@@ -0,0 +1,7 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height="17" Width="13">
<Path Fill="#0f0f0f" Data="M2.7 7.3L1.4 6l5-5H10v2h1V0H6L0 6l2 2 .7-.7zM8 6l-6 6 5 5 6-6V6zm4 4.6l-5 5L3.4 12l5-5H12z" />
<Path Fill="#0f0f0f" Data="M3.7 10.3L2.4 9l5-5H11v2h1V3H7L1 9l2 2 .7-.7z" />
<Ellipse Fill="#0f0f0f" Canvas.Left="10" Canvas.Top="8" Width="1" Height="1" />
</Canvas>
</Viewbox>
+21
View File
@@ -0,0 +1,21 @@
<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Viewbox.Resources>
<LinearGradientBrush x:Key="a24f9983-911f-4df7-920f-f964c8c10f82" StartPoint="0.0900000035762787,0.158340007066727" EndPoint="0.0900000035762787,0.0578799992799759" MappingMode="RelativeToBoundingBox">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="#32bedd" />
<GradientStop Offset="0.175" Color="#32caea" />
<GradientStop Offset="0.41" Color="#32d2f2" />
<GradientStop Offset="0.775" Color="#32d4f5" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Viewbox.Resources>
<Canvas Height="18" Width="18">
<Path Fill="{StaticResource a24f9983-911f-4df7-920f-f964c8c10f82}" Data="M.5 5.788h17v9.478a.568.568 0 01-.568.568H1.068a.568.568 0 01-.568-.568V5.788z" />
<Path Fill="#0078d4" Data="M1.071 2.166h15.858a.568.568 0 01.568.568v3.054H.5V2.734a.568.568 0 01.571-.568z" />
<Path Fill="#f2f2f2" Data="M2.825 7.979l.369-.37a.167.167 0 01.236-.001l2.732 2.724a.335.335 0 010 .474l-.368.37-2.968-2.96a.167.167 0 010-.237z" />
<Path Fill="#e6e6e6" Data="M3.249 13.504l-.37-.37a.167.167 0 010-.236l2.916-2.925.37.368a.335.335 0 010 .474l-2.683 2.691a.167.167 0 01-.236 0z" />
<Rectangle Width="4.771" Height="1.011" Canvas.Left="7.221" Canvas.Top="12.64" Fill="#f2f2f2" RadiusX=".291" RadiusY=".291" />
</Canvas>
</Viewbox>

Some files were not shown because too many files have changed in this diff Show More