UPDATE 2024.08.18: This post has been archived. Please use Copy-PnPList, which allows an existing list to be copied to either the same site or to another site (same tenant).
Invoke-PnPSiteTemplate allows, between others, copying lists and libraries between sites.
Duplicating a list requires, however, a small modification of the exported template.
First, export the list to a variable:
$template = Get-PnPSiteTemplate -OutputInstance -ListsToExtract $listName -Handlers Lists
Since the duplicated list must have new url, the template must be updated to change the list, forms and views urls:
for ($i = 0; $i -lt $data.Value.Count; $i++) {
$data.Value[$i].Title = $newName
$data.Value[$i].Url = "Lists/$newName"
$data.Value[$i].DefaultDisplayFormUrl = ($template.Lists[0].DefaultDisplayFormUrl -replace "/$listName/", "/$newName/")
$data.Value[$i].DefaultEditFormUrl = ($template.Lists[0].DefaultEditFormUrl -replace "/$listName/", "/$newName/")
$data.Value[$i].DefaultNewFormUrl = ($template.Lists[0].DefaultNewFormUrl -replace "/$listName/", "/$newName/")
for ($j = 0; $j -lt $data.Value[$i].Views.Count; $j++) {
$data.Value[$i].Views[$j].SchemaXml = ($data.Value[$i].Views[$j].SchemaXml -replace "/$listName/", "/$newName/")
}
}
If the list contains calculated fields, these must be updated as well to avoid errors during provisioning. It's a knowns issue and the code below is inspired by the code sample, using regex for simplicity
for ($i = 0; $i -lt $data.Value.Count; $i++) {
$list = $data.Value[$i]
$lookupField = @{}
$list.Fields | ForEach-Object { $schema = [xml]$_.SchemaXml; $lookupField[$schema.Field.Name] = $schema.Field.DisplayName}
$list.FieldRefs | ForEach-Object { $lookupField[$_.Name] = $_.DisplayName}
# Find all calculated fields. Because of a PnP bug, a template uses [{fieldtitle:<internalFieldName>}] in calculated field formulas and not the field's Display Name
# The code below finds all instances of {fieldtitle:xxxx} in the field's formula, figures out all of the internal names used (the xxxx after the "fieldtitle:"),
# looks up the field's Display Name, and replaces {fieldtitle:xxxx} in the calculation with the field's Display Name.
for ($j = 0; $j -lt $list.Fields.Count; $j++) {
$results = ([Regex]::Matches($list.Fields[$j].SchemaXml, '(?<={fieldtitle:)(.*?)(?=})') | Select-Object Value).Value | Get-Unique
foreach ($field in $results) {
$displayName = $lookupField[$field]
"$results - $displayName"
$data.Value[$i].Fields[$j].SchemaXml = $data.Value[$i].Fields[$j].SchemaXml.Replace("{fieldtitle:$($field)}", $displayName)
}
}
}
Full code:
function Copy-List{
param (
[string]$listName,
[string]$newName
)
function Set-Formulas{
param(
[ref]$data
)
Write-Host "Set-Formulas"
# We're using a for loop instead of a foreach so that we can have an index that we can use to update the original $template object
for ($i = 0; $i -lt $data.Value.Count; $i++) {
$list = $data.Value[$i]
$lookupField = @{}
$list.Fields | ForEach-Object { $schema = [xml]$_.SchemaXml; $lookupField[$schema.Field.Name] = $schema.Field.DisplayName}
$list.FieldRefs | ForEach-Object { $lookupField[$_.Name] = $_.DisplayName}
# Find all calculated fields. Because of a PnP bug, a template uses [{fieldtitle:<internalFieldName>}] in calculated field formulas and not the field's Display Name
# The code below finds all instances of {fieldtitle:xxxx} in the field's formula, figures out all of the internal names used (the xxxx after the "fieldtitle:"),
# looks up the field's Display Name, and replaces {fieldtitle:xxxx} in the calculation with the field's Display Name.
for ($j = 0; $j -lt $list.Fields.Count; $j++) {
$results = ([Regex]::Matches($list.Fields[$j].SchemaXml, '(?<={fieldtitle:)(.*?)(?=})') | Select-Object Value).Value | Get-Unique
foreach ($field in $results) {
$displayName = $lookupField[$field]
"$results - $displayName"
$data.Value[$i].Fields[$j].SchemaXml = $data.Value[$i].Fields[$j].SchemaXml.Replace("{fieldtitle:$($field)}", $displayName)
}
}
}
}
function Set-ListViews{
param(
[ref]$data,
[string]$listName,
[string]$newName
)
Write-Host "Set-ListViews"
for ($i = 0; $i -lt $data.Value.Count; $i++) {
$data.Value[$i].Title = $newName
$data.Value[$i].Url = "Lists/$newName"
$data.Value[$i].DefaultDisplayFormUrl = ($template.Lists[0].DefaultDisplayFormUrl -replace "/$listName/", "/$newName/")
$data.Value[$i].DefaultEditFormUrl = ($template.Lists[0].DefaultEditFormUrl -replace "/$listName/", "/$newName/")
$data.Value[$i].DefaultNewFormUrl = ($template.Lists[0].DefaultNewFormUrl -replace "/$listName/", "/$newName/")
for ($j = 0; $j -lt $data.Value[$i].Views.Count; $j++) {
$data.Value[$i].Views[$j].SchemaXml = ($data.Value[$i].Views[$j].SchemaXml -replace "/$listName/", "/$newName/")
}
}
}
Write-Host "Duplicating list $listName"
$currentFolder = Split-Path -Parent $PSCommandPath
$template = Get-PnPSiteTemplate -OutputInstance -ListsToExtract $listName -Handlers Lists
Set-ListViews -data ([ref]$template.Lists) -listName $listName -newName $newName
Set-Formulas -data ([ref]$template.Lists)
Save-PnPSiteTemplate -Template $template -Out "$currentFolder/temp/$newName.xml" -Force
Invoke-PnPSiteTemplate -Path "$currentFolder/temp/$newName.xml"
}
Using the function is as easy as:
Copy-List -listName "List" -newName "ListDuplicate"
Top comments (0)