Guides & Tools
SHARING IS CARING
Patterns & Practices
Collection of tools and guidance meant to help you extend Microsoft 365 to your needs following best practices.
Don’t reinvent the wheel — focus on what truly matters for your organization.
Learn. Reuse. Share.


Consider when Migrating SharePoint Site
https:/pnp.github.io/powershell/cmdlets/Copy-PnPFile.html
Move-PnPFolder | PnP PowerShell
https:/pnp.github.io/powershell/cmdlets/Move-PnPFolder.html
Migrating SharePoint Site
This PowerShell will migrate the site to a Document Library for Archiving
#Create a form to collect the site name, subsite name, sub-subsite name, and document library name
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# Download the image
if($null -eq $imagePath){
$imageUrl = "https://cspowerapplicationsstor.blob.core.windows.net/repository/cleanLogo.png"
$imagePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "cleanLogo.png")
Invoke-WebRequest -Uri $imageUrl -OutFile $imagePath}
# Create the form
$form = New-Object System.Windows.Forms.Form
$form.Text = "Copy Site Data to sites/Archive"
$form.Size = New-Object System.Drawing.Size(300, 500)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
$form.MaximizeBox = $false
$form.MinimizeBox = $false
# Create a label for site name
$labelForm = New-Object System.Windows.Forms.Label
$labelForm.Text = "Fill out approprate fields"
$labelForm.Size = New-Object System.Drawing.Size(280, 20)
$labelForm.Location = New-Object System.Drawing.Point(10, 60)
$labelForm.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelForm.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelForm)
# Create a label for site name
$labelForm2 = New-Object System.Windows.Forms.Label
$labelForm2.Text = "Library Will Remain Until Renamed"
$labelForm2.Size = New-Object System.Drawing.Size(280, 20)
$labelForm2.Location = New-Object System.Drawing.Point(10, 90)
$labelForm2.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelForm2.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelForm2)
# Load and add the image
$image = [System.Drawing.Image]::FromFile($imagePath)
$pictureBox = New-Object System.Windows.Forms.PictureBox
$pictureBox.Image = $image
$pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::CenterImage
$pictureBox.Size = New-Object System.Drawing.Size(260, 40)
$pictureBox.Location = New-Object System.Drawing.Point(10, 10)
$form.Controls.Add($pictureBox)
# Create a label for site name
$labelSite = New-Object System.Windows.Forms.Label
$labelSite.Text = "Site Name:"
$labelSite.Size = New-Object System.Drawing.Size(280, 20)
$labelSite.Location = New-Object System.Drawing.Point(10, 120)
$labelSite.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelSite.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelSite)
# Create a text box for site name
$textBoxSite = New-Object System.Windows.Forms.TextBox
$textBoxSite.Size = New-Object System.Drawing.Size(260, 20)
$textBoxSite.Location = New-Object System.Drawing.Point(10, 150)
$form.Controls.Add($textBoxSite)
# Create a label for subsite name
$labelSubsite = New-Object System.Windows.Forms.Label
$labelSubsite.Text = "Subsite Name (optional):"
$labelSubsite.Size = New-Object System.Drawing.Size(280, 20)
$labelSubsite.Location = New-Object System.Drawing.Point(10, 180)
$labelSubsite.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelSubsite.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelSubsite)
# Create a text box for subsite name
$textBoxSubsite = New-Object System.Windows.Forms.TextBox
$textBoxSubsite.Size = New-Object System.Drawing.Size(260, 20)
$textBoxSubsite.Location = New-Object System.Drawing.Point(10, 210)
$form.Controls.Add($textBoxSubsite)
# Create a label for sub-subsite name
$labelSubSubsite = New-Object System.Windows.Forms.Label
$labelSubSubsite.Text = "Sub-Subsite Name (optional):"
$labelSubSubsite.Size = New-Object System.Drawing.Size(280, 20)
$labelSubSubsite.Location = New-Object System.Drawing.Point(10, 240)
$labelSubSubsite.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelSubSubsite.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelSubSubsite)
# Create a text box for sub-subsite name
$textBoxSubSubsite = New-Object System.Windows.Forms.TextBox
$textBoxSubSubsite.Size = New-Object System.Drawing.Size(260, 20)
$textBoxSubSubsite.Location = New-Object System.Drawing.Point(10, 270)
$form.Controls.Add($textBoxSubSubsite)
# Create a label for document Library name
$labeldocLibrary = New-Object System.Windows.Forms.Label
$labeldocLibrary.Text = "Document Library: $documentLibrary"
$labeldocLibrary.Size = New-Object System.Drawing.Size(280, 20)
$labeldocLibrary.Location = New-Object System.Drawing.Point(10, 300)
$labeldocLibrary.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labeldocLibrary.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labeldoclibrary)
# Create a text box for document Library name
$textdocLibrary = New-Object System.Windows.Forms.TextBox
$textdocLibrary.Size = New-Object System.Drawing.Size(260, 20)
$textdocLibrary.Location = New-Object System.Drawing.Point(10, 330)
$textdocLibrary.Text = $documentLibrary # Set default value
$form.Controls.Add($textdocLibrary)
# Create a button
$button = New-Object System.Windows.Forms.Button
$button.Text = "OK"
$button.Size = New-Object System.Drawing.Size(75, 23)
$button.Location = New-Object System.Drawing.Point(100, 370)
$form.Controls.Add($button)
# Add event handler for text box text changed event
$textdocLibrary.Add_TextChanged({
if ([string]::IsNullOrWhiteSpace($textdocLibrary.Text)) {
$button.Enabled = $false # Enable the button if text box is not empty
} else {
$button.Enabled = $true # Disable the button if text box is empty
}
})
if ($null -eq $documentLibrary) {
$button.Enabled = $false # Disable the button initially
}
# Define the button click event
$button.Add_Click({
$form.Tag = @{
SubSitesiteName = $textBoxSubSubsite.Text
SiteName = $textBoxSite.Text
SubsiteName = $textBoxSubsite.Text
DocLibrary = $textdocLibrary.Text
}
$form.Close()
})
# Show the form
$form.ShowDialog()
# Output the collected data
$form.Tag
# Get the site and subsite names from the form
$siteName = $form.Tag.SiteName -replace '\s', ''
# Output the site name
Write-Host "Site Name entered: $siteName"
$subsiteName = $form.Tag.SubsiteName -replace '\s', ''
if ($form.Tag.SubsiteName) {
Write-Host "Site Name entered: $subsiteName"
}
$subSubsiteName = $form.Tag.SubSitesiteName -replace '\s', ''
if ($form.Tag.SubSitesiteName) {
Write-Host "Site Name entered: $subSubsiteName"
}
if ($form.Tag.DocLibrary) {
$documentLibrary = $form.Tag.DocLibrary -replace '\s', ''
}
else {
$documentLibrary = $documentLibrary
}
Write-Host "Document Library: $documentLibrary"
#Set Variables
$contentType = ""
$listsFolder = ""
$clinetId = "<app id>"
$tenant = "domain.onmicrosoft.com"
$thumbprint = "cert thumbprint"
$siteBaseUrl = "https://domain.sharepoint.com/"
$siteDestinationURL = "sites/Archive"
$siteMigrateUrl = $siteBaseUrl + $siteDestinationURL
$siteSourceBaseUrl = if ($subsiteName) {
if ($subSubsiteName) {
$siteName + "/" + $subsiteName + "/" + $subSubsiteName
} else {
$siteName + "/" + $subsiteName
}
} else {
$siteName
}
$subsite = $siteSourceBaseUrl -replace "/", "_"
#$siteSourceBaseUrl = $form.Tag.SiteName
$siteDestinationFolder = "$documentLibrary/$subsite"
$listsFolder = $siteDestinationFolder + "/Lists"
$doclistName = $siteDestinationFolder.Split('/')[0]
$siteDestinationRoot = $siteDestinationURL + "/" + $siteDestinationFolder -replace "/$subsite", ''
$siteDestinationFullPath = $siteDestinationURL + "/" + $siteDestinationFolder
$siteUrl = $siteBaseUrl + $siteSourceBaseUrl
$contentType = $subsite + "ContentTypeTemplate.xml"
# Connect to the destination site
Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
# Check if the document library exists in the destination site
$libraryExists = Get-PnPList -Identity $documentLibrary -ErrorAction SilentlyContinue
if (-not $libraryExists) {
# Create the document library in the destination site
New-PnPList -Title $documentLibrary -Template DocumentLibrary
}
# Create the field Creators and Last Update by if it doesn't exist
# Check if the field exists
$field = Get-PnPField -List $doclistName -Identity "LastUpdateBy" -ErrorAction SilentlyContinue
if ($null -eq $field) {
Add-PnPField -List $doclistName -DisplayName "LastUpdateBy" -InternalName "LastUpdateBy" -Type Text
Write-Host "Field 'LastUpdateBy' created."
} else {
Write-Host "Field 'LastUpdateBy' already exists."
}
$field = Get-PnPField -List $doclistName -Identity "Creator" -ErrorAction SilentlyContinue
if ($null -eq $field) {
Add-PnPField -List $doclistName -DisplayName "Creator" -InternalName "Creator" -Type Text
Write-Host "Field 'Creator' created."
} else {
Write-Host "Field 'Creator' already exists."
}
# Check if the folder exists in the destination site
$folderExists = Get-PnPFolder -Url $siteDestinationFolder -ErrorAction SilentlyContinue
if (-not $folderExists) {
# Create the folder in the destination site
Add-PnPFolder -Name $subsite -Folder "/$siteDestinationRoot"
Add-PnPFolder -Name "Lists" -Folder "$siteDestinationFolder"
}
# Collect the folders and files from the source site
try {
# Connect to the source site
Connect-PnPOnline -Url $siteUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
# Retrieve all lists including the properties you'll need for filtering.
$lists = Get-PnPList -Includes Title, BaseTemplate, Hidden, RootFolder.ServerRelativeUrl
$documentLibraries = $lists | Where-Object {
$_.BaseTemplate -eq 101 -or
(-not $_.Hidden) -and ($_.Title -notmatch 'Health & Safety Documents') -and
($_.Title -notmatch 'Forms') -and ($_.Title -notmatch 'Workflow Tasks') -and
($_.RootFolder.ServerRelativeUrl -notmatch 'Lists')
}
# Initialize an array to store all folders
$folders = @()
$allfolders = @()
$docLibraries = @()
$folderfiles = @()
$topDocumentFolders = @()
$listsSections = @()
$files1stLevel = @()
$rootLists = @()
# Loop through each document library and get folders
foreach ($library in $documentLibraries) { #$library = $documentLibraries[1]
$libraryRootFolder = Get-PnPFolder -Url $library.RootFolder.ServerRelativeUrl
$RootCellar = $libraryRootFolder.Name #$library.Title
$docLibraries += $libraryRootFolder
# Get all top level folders in the document library
$docsRootDir = Get-PnPFolderItem -FolderSiteRelativeUrl $RootCellar -ItemType Folder
$docsRootDir = $docsRootDir | Where-Object { $_.Name -ne "Forms" }
$topDocumentFolders += $docsRootDir
# Get all top level files in the document library
$libraryItems = Get-PnPFolderItem -FolderSiteRelativeUrl $RootCellar -ItemType File -Recursive
$libraryLevelFiles = Get-PnPFolderItem -FolderSiteRelativeUrl $RootCellar -ItemType File
$siteFiles = $libraryItems | Where-Object {$libraryLevelFiles -notcontains $_}
# Acquire the properties of the files
foreach ($item in $libraryLevelFiles) {
$listItem = Get-PnPProperty -ClientObject $item -Property ListItemAllFields
if ($null -ne $listItem.Id){ #($listItem -and $null -ne $listItem.Id) {
$folderfiles += $listItem
$files1stLevel += $listItem
}
}
foreach ($item in $siteFiles) {
$fileItem = Get-PnPProperty -ClientObject $item -Property ListItemAllFields
if ($null -ne $fileItem.Id) {
$folderfiles += $fileItem
}
}
}
# Retrieve all lists and libraries
$lists = Get-PnPList
# Filter lists that are in the root directory
$rootLists = $lists | Where-Object {-not $_.Hidden -and $_.Title -notmatch "Tasks" -and $_.Title -notmatch "Documents"}
$listFolders = $rootLists | Where-Object {$_.RootFolder.ServerRelativeUrl -notmatch "Lists"}
$listsSections = $rootLists | Where-Object {$listFolders -notcontains $_}
# Extract the content types and lists from the source site
Get-PnPSiteTemplate -Out $contentType -Handlers ContentTypes -Force
}
catch {
# Handle the error
Write-Host "Error details: $_"
exit
}
# Connect to the destination site
Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
# Apply the template to the destination site
# Invoke-PnPSiteTemplate -Path $contentType -Verbose
# Create the document librarry folders in the destination site
foreach ($library in $docLibraries) {
$RootCellar = $library.Name
$libraryLanding = $siteDestinationFolder + "/" + $RootCellar
# Check if the folder exists in the destination site
$folderExists = Get-PnPFolder -Url $libraryLanding -ErrorAction SilentlyContinue
if (-not $folderExists) {
# Create the folder in the destination site
Add-PnPFolder -Name $RootCellar -Folder $siteDestinationFolder
}
$allfolders += $library
}
# Copy each file to the root directories in destination site
foreach ($file in $files1stLevel) {
$fieldValues = $file.FieldValues
$title = $fieldValues["Title"]
#$pageName = $fieldValues["FileLeafRef"]
$SourcePageURL = $fieldValues["FileRef"]
$SourceUrl = $fieldValues["FileDirRef"]
$pageLanding = $SourceUrl -replace "^/$siteSourceBaseUrl", $siteDestinationFolder
$pagetoCopy = $SourcePageURL -replace "^/$siteSourceBaseUrl", "$siteDestinationFolder"
# Check if the file exists in the destination site
$fileExists = Get-PnPFile -Url $pagetoCopy -ErrorAction SilentlyContinue
if ($fileExists) {
Write-Host "File already exists in the destination site." -ForegroundColor Yellow
continue
}
else {
Copy-PnPFile -SourceUrl $SourcePageURL -TargetUrl $pageLanding -Force
Write-Host "File copied successfully."
}
}
# Add the folder depth field values to the files
Function InitializeAndAddField {
param (
[array]$folders,
[string]$fieldName
)
$folders | ForEach-Object {
if (-not $_.PSObject.Properties[$fieldName]) {
$_ | Add-Member -MemberType NoteProperty -Name $fieldName -Value ($_.ServerRelativeUrl -split '/').Count
} else {
$_.$fieldName = ($_.ServerRelativeUrl -split '/').Count
}
}
return $folders
}
$topDocumentFolders = InitializeAndAddField -folders $topDocumentFolders -fieldName "FolderDepth"
$topDocumentFolders = $topDocumentFolders | Where-Object { $_.Name -ne "Forms" }
foreach($top in $topDocumentFolders){
Write-Host "Folder: $($top.ServerRelativeUrl) Depth: $($top.FolderDepth)"
}
# Function to recursively process folders and their subfolders
function Copy-FoldersRecursively {
param (
[array]$folders
)
foreach ($folder in $folders) {
# Process the current folder
$folderUrl = $folder.ServerRelativeUrl -replace "^/$siteDestinationFullPath", "/$siteSourceBaseUrl"
$lastSlashIndex = $folderUrl.LastIndexOf('/')
$folderPath = $folderUrl.Substring(0, $lastSlashIndex)
$itemCount = $folder.ItemCount
Write-Output "Processing folder: $folderUrl"
# Replace the source URL with the destination URL
$destinationUrl = $folderPath -replace "^/$siteSourceBaseUrl", "$siteDestinationFolder"
$nextLevelFolder = $folderUrl -replace "^/$siteSourceBaseUrl", "$siteDestinationFolder"
try {
if($itemCount -ne 0){
Copy-PnPFolder -SourceUrl $folderUrl -TargetUrl $destinationUrl -Force -Overwrite
Write-Host "Copied folder from $folderUrl to $destinationUrl" -ForegroundColor Green
} else {
# Create the empty folder at the destination
Add-PnPFolder -Name (Split-Path $folderUrl -Leaf) -Folder $destinationUrl -ErrorAction SilentlyContinue
Write-Host "Created empty folder at $destinationUrl" -ForegroundColor Yellow
}
} catch {
Write-Host "Error copying or creating folder from $folderUrl to $destinationUrl" -ForegroundColor Blue
Write-Host "Error details: $_" -ForegroundColor Magenta
}
Start-Sleep -Seconds 5
# Get subfolders of the current folder
$subfolders = Get-PnPFolderItem -FolderSiteRelativeUrl $nextLevelFolder -ItemType Folder -ErrorAction SilentlyContinue
if ($subfolders) {
# Log retrieved subfolders for debugging
Write-Output "Retrieved subfolders for $($folderUrl): $($subfolders | ForEach-Object { $_.ServerRelativeUrl })"
# Filter out subfolders named "Forms"
$filteredSubfolders = $subfolders | Where-Object { $_.Name -ne "Forms" }
# Recursively process subfolders
Copy-FoldersRecursively -folders $filteredSubfolders
} else {
Write-Output "No subfolders found for $folderUrl"
}
}
}
# Measure the execution time of the Copy-FoldersRecursively function
$executionTime = Measure-Command {
Copy-FoldersRecursively -folders $topDocumentFolders
}
# Display the elapsed time
Write-Host "The Copy-FoldersRecursively function took $($executionTime.TotalSeconds) seconds to complete."
# If needed, add a sleep based on the measured time
if ($executionTime.TotalSeconds -lt 5) {
Write-Host "Adding a sleep of 5 seconds to ensure all files are available for update."
Start-Sleep -Seconds 5
}
foreach ($item in $folderfiles) { #$item = $folderfiles[10]
$fieldValues = $null
$fieldValues = $item.FieldValues
$title = $fieldValues["Title"]
$itemId = $fieldValues["ID"]
if($null -ne $itemId) {
$userEditor = ""
$userCreator = ""
# Access metadata
$created = $fieldValues["Created"]
$modified = $fieldValues["Modified"]
$author = $fieldValues["Author"]
$editor = $fieldValues["Editor"]
$userCreated = $author.Email
$userEdited = $editor.Email
$fileName = $fieldValues["FileLeafRef"]
$fileUrl = $fieldValues["FileDirRef"]
$destinationUrlFile = $fileUrl -replace "^/$siteSourceBaseUrl", "/$siteDestinationFullPath"
#$archiveFolder = $siteDestinationFullPath -replace "/$subsite", ""
$DestinationFileName = $destinationUrlFile + "/" + $fileName
$NewFile = Get-PnPFile -Url $DestinationFileName -AsListItem
if ($null -ne $NewFile.Id) {
$values = @()
# Prepare the values to update
$values = @{
"Creator" = $userCreated
"Created" = $created
"LastUpdateBy" = $userEdited
"Modified" = $modified
}
$pnpUser = "i:0#.f|membership|" + $userEdited
$userEditor = Get-PnPUser -Identity $pnpUser -ErrorAction SilentlyContinue
if ($userEditor) {
$values["Editor"] = $userEdited
}
$pnpCreator = "i:0#.f|membership|" + $userCreated
$userCreator = Get-PnPUser -Identity $pnpCreator -ErrorAction SilentlyContinue
if ($userCreator) {
$values["Author"] = $userCreated
}
# Update the metadata on the new file with the modified and modified by source values
Write-Host "Updating metadata for file: $DestinationFileName with ID: $($NewFile.Id)"
Set-PnPListItem -List $doclistName -Identity $NewFile.Id -Values $values
}
}
}
# Copy the lists and libraries to the destination site
# Connect to the source site
Connect-PnPOnline -Url $siteUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
foreach($root in $listsSections | Where-Object { $_.Title -notmatch "QuickLinks" -and $_.Title -notmatch "Page Content" -and $_.Title -notmatch "Microfeed"}){
# Define the SharePoint list name
$listName = ""
$listXml = ""
$listName = $root.Title
$listXml = $subsite + $listName + ".xml"
#Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
# Extract the content types and lists from the source site
Get-PnPSiteTemplate -Out $listXml -ListsToExtract $listName -Handlers Lists -Force # $listName -Handlers Lists
Add-PnPDataRowsToSiteTemplate -Path $listXml -List $listName
}
# Connect to the destination site
Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
foreach($root in $listsSections | Where-Object { $_.Title -notmatch "QuickLinks" -and $_.Title -notmatch "Page Content" -and $_.Title -notmatch "Microfeed"}){
# Define the SharePoint list name
$listName = ""
$listCreating = ""
$listUrl = ""
$listXml = ""
$listName = $root.Title
$listCreating = $subsite + "_" + $listName
$listUrl = $siteUrl + "/Lists/" + $listName
$listXml = $subsite + $listName + ".xml"
# Upload the template XML to the destination site
if($listXml -ne "") {
Add-PnPFile -Path $listXml -Folder $listsFolder
<#try {
Set-PnPList -Identity $listName -Title $listCreating
Write-Output "Successfully renamed list: $listName to $listCreating"
} catch {
Write-Output "Failed to rename list: $listName"
Write-Output "Error: $($_.Exception.Message)"
}#>
Remove-Item -Path $listXml -Force
}
}
Add-PnPFile -Path $contentType -Folder $listsFolder
Remove-Item -Path $contentType -ForceGet approval for changes in SharePoint lists by Power Automate
Using Get Changes action we ca determine what happened to an item, but it takes some steps to have a clean output.
If Manager Out
# Manager name
if(equals(outputs('Compose_Area_Manager_Status'),'disabled'), triggerBody()?['RegionalCenterManager'],triggerBody()?['RVPO'])
# Manager email
if(equals(outputs('Compose_Area_Manager_Status'),'disabled'), triggerBody()?['RCM_Email'],triggerBody()?['RVPO_Email'])Rejecting Manager
outputs('Post_adaptive_card_to_Power_Platform')?['body']?['responder']?['displayName']Refunds
Understand how to handle refunds, manage disputes, and maintain positive customer relationships during payment issues.
FAQs
Enhance your architectural journey with the Études Architect app.
- Collaborate with fellow architects.
- Showcase your projects.
- Experience the world of architecture.

