DEV Community

Kinga
Kinga

Posted on • Edited on

Duplicate list using PnP Powershell

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/")
            }
        }
Enter fullscreen mode Exit fullscreen mode

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)
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

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" 
}
Enter fullscreen mode Exit fullscreen mode

Using the function is as easy as:
Copy-List -listName "List" -newName "ListDuplicate"

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay