Public/Export-M365PPlan.ps1
|
function Export-M365PPlan { <# .SYNOPSIS Exports a complete Microsoft 365 Planner plan to JSON or XML format. .DESCRIPTION The Export-M365PPlan function exports a complete Planner plan including all metadata, buckets, tasks, assignments, and checklist items to a structured JSON or XML file. This is useful for backup, documentation, migration, or version control purposes. .PARAMETER PlanId The unique identifier (GUID) of the plan to export. .PARAMETER OutputPath The file path where the exported plan will be saved. The file extension (.json or .xml) will be added automatically based on the Format parameter if not provided. .PARAMETER Format The output format for the exported plan. Valid values are 'JSON' (default) or 'XML'. .PARAMETER IncludeCompleted If specified, completed tasks will be included in the export. By default, only incomplete tasks are exported. .PARAMETER Force If specified, overwrites the output file if it already exists without prompting. .EXAMPLE Export-M365PPlan -PlanId "abc123def456" -OutputPath ".\backup.json" Exports the specified plan to a JSON file named backup.json in the current directory. .EXAMPLE Export-M365PPlan -PlanId "abc123def456" -OutputPath ".\backup" -Format XML -IncludeCompleted Exports the plan including completed tasks to an XML file named backup.xml. .EXAMPLE Get-MgGroupPlannerPlan -GroupId "group123" | Export-M365PPlan -OutputPath ".\exports" -Format JSON Exports all plans from a specific group to separate JSON files in the exports directory. .NOTES Author: Sergio Cánovas Cardona Version: 1.1.0 Requires: Microsoft.Graph.Planner module The exported file contains complete plan structure that can be imported with Import-M365PPlan. .LINK Import-M365PPlan Copy-M365PPlan #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Id')] [string]$PlanId, [Parameter(Mandatory = $true)] [string]$OutputPath, [Parameter(Mandatory = $false)] [ValidateSet('JSON', 'XML')] [string]$Format = 'JSON', [Parameter(Mandatory = $false)] [switch]$IncludeCompleted, [Parameter(Mandatory = $false)] [switch]$Force ) begin { Write-Verbose "Starting plan export process..." # Ensure output directory exists $outputDir = Split-Path -Path $OutputPath -Parent if ($outputDir -and -not (Test-Path -Path $outputDir)) { Write-Verbose "Creating output directory: $outputDir" New-Item -Path $outputDir -ItemType Directory -Force | Out-Null } } process { try { # Step 1: Get plan details Write-Verbose "Retrieving plan details for Plan ID: $PlanId" $plan = Get-MgPlannerPlan -PlannerPlanId $PlanId -ErrorAction Stop if (-not $plan) { throw "Plan with ID '$PlanId' not found." } Write-Verbose "Plan found: $($plan.Title)" # Step 2: Get all buckets Write-Verbose "Retrieving buckets..." $buckets = Get-MgPlannerPlanBucket -PlannerPlanId $PlanId -All -ErrorAction Stop Write-Verbose "Found $($buckets.Count) bucket(s)" # Step 3: Get all tasks Write-Verbose "Retrieving tasks..." $allTasks = Get-MgPlannerPlanTask -PlannerPlanId $PlanId -All -ErrorAction Stop # Filter completed tasks if not included if (-not $IncludeCompleted) { $tasks = $allTasks | Where-Object { $_.PercentComplete -ne 100 } Write-Verbose "Found $($tasks.Count) incomplete task(s) (total: $($allTasks.Count))" } else { $tasks = $allTasks Write-Verbose "Found $($tasks.Count) task(s)" } # Step 4: Get task details (including checklist items) Write-Verbose "Retrieving detailed task information..." $taskDetails = @() foreach ($task in $tasks) { try { $taskDetail = Get-MgPlannerTaskDetail -PlannerTaskId $task.Id -ErrorAction Stop $taskDetails += @{ Task = $task Details = $taskDetail } } catch { Write-Warning "Could not retrieve details for task '$($task.Title)': $_" } } # Step 5: Build export structure $exportData = [PSCustomObject]@{ ExportedAt = Get-Date -Format 'o' ExportedBy = (Get-MgContext).Account ModuleVersion = '1.1.0' Plan = @{ Id = $plan.Id Title = $plan.Title CreatedDateTime = $plan.CreatedDateTime Owner = $plan.Owner Container = $plan.Container } Buckets = @($buckets | ForEach-Object { @{ Id = $_.Id Name = $_.Name OrderHint = $_.OrderHint PlanId = $_.PlanId } }) Tasks = @($taskDetails | ForEach-Object { @{ Id = $_.Task.Id Title = $_.Task.Title BucketId = $_.Task.BucketId PlanId = $_.Task.PlanId PercentComplete = $_.Task.PercentComplete Priority = $_.Task.Priority StartDateTime = $_.Task.StartDateTime DueDateTime = $_.Task.DueDateTime CompletedDateTime = $_.Task.CompletedDateTime Assignments = $_.Task.Assignments.AdditionalProperties OrderHint = $_.Task.OrderHint Description = $_.Details.Description Checklist = $_.Details.Checklist.AdditionalProperties References = $_.Details.References.AdditionalProperties PreviewType = $_.Details.PreviewType } }) Statistics = @{ TotalBuckets = $buckets.Count TotalTasks = $tasks.Count CompletedTasksIncluded = $IncludeCompleted.IsPresent TotalTasksInPlan = $allTasks.Count } } # Step 6: Determine output file path $extension = if ($Format -eq 'JSON') { '.json' } else { '.xml' } if (-not $OutputPath.EndsWith($extension)) { $OutputPath = $OutputPath -replace '\.(json|xml)$', '' $OutputPath = "$OutputPath$extension" } # Check if file exists if ((Test-Path -Path $OutputPath) -and -not $Force) { $shouldContinue = $PSCmdlet.ShouldContinue( "File '$OutputPath' already exists. Overwrite?", "Confirm Overwrite" ) if (-not $shouldContinue) { Write-Warning "Export cancelled by user." return } } # Step 7: Export to file if ($PSCmdlet.ShouldProcess($OutputPath, "Export plan '$($plan.Title)' as $Format")) { Write-Verbose "Exporting to: $OutputPath" if ($Format -eq 'JSON') { $exportData | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8 -Force } else { # Convert to XML $xmlDoc = New-Object System.Xml.XmlDocument $declaration = $xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", $null) $xmlDoc.AppendChild($declaration) | Out-Null function ConvertTo-XmlElement { param($Object, $ElementName, $XmlDoc, $ParentElement) $element = $XmlDoc.CreateElement($ElementName) if ($Object -is [System.Collections.IEnumerable] -and $Object -isnot [string]) { foreach ($item in $Object) { ConvertTo-XmlElement -Object $item -ElementName 'Item' -XmlDoc $XmlDoc -ParentElement $element } } elseif ($Object -is [System.Collections.IDictionary] -or $Object.PSObject.Properties) { $props = if ($Object -is [System.Collections.IDictionary]) { $Object.Keys } else { $Object.PSObject.Properties.Name } foreach ($prop in $props) { $value = if ($Object -is [System.Collections.IDictionary]) { $Object[$prop] } else { $Object.$prop } if ($null -ne $value) { if ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) { $childElement = ConvertTo-XmlElement -Object $value -ElementName $prop -XmlDoc $XmlDoc -ParentElement $element $element.AppendChild($childElement) | Out-Null } elseif ($value -is [System.Collections.IDictionary] -or $value.PSObject.Properties.Count -gt 0) { $childElement = ConvertTo-XmlElement -Object $value -ElementName $prop -XmlDoc $XmlDoc -ParentElement $element $element.AppendChild($childElement) | Out-Null } else { $element.SetAttribute($prop, $value.ToString()) } } } } else { $element.InnerText = $Object.ToString() } return $element } $rootElement = ConvertTo-XmlElement -Object $exportData -ElementName 'PlannerPlanExport' -XmlDoc $xmlDoc -ParentElement $xmlDoc $xmlDoc.AppendChild($rootElement) | Out-Null $xmlDoc.Save($OutputPath) } Write-Verbose "Export completed successfully" Write-Host "Plan '$($plan.Title)' exported to: $OutputPath" -ForegroundColor Green Write-Host " - Buckets: $($buckets.Count)" -ForegroundColor Cyan Write-Host " - Tasks: $($tasks.Count)" -ForegroundColor Cyan Write-Host " - Format: $Format" -ForegroundColor Cyan # Return file info Get-Item -Path $OutputPath } } catch { Write-Error "Failed to export plan: $_" throw } } end { Write-Verbose "Export process completed" } } |