If you are still using the PnP-Sites-Core library (consider migrating to the new version: PnP Framework!) and using access tokens to connect to SharePoint, there may be scenarios where you will get the error message: IDX12729: Unable to decode the header ‘[PII]’ is hidden’ as Base64Url encoded string.
Continue reading “IDX12729: Unable to decode the header ‘[PII]’ is hidden’”Category: PnP
Provision folder structure for Files tab in Teams
The Teams Files tab is always empty when we create a new channel. But this can be controlled to include a folder structure or other content that is surfaced in Teams.
Continue reading “Provision folder structure for Files tab in Teams”Merge PDF files in SharePoint using an Azure Function
Need to merge PDF files stored in SharePoint? Look no further!
In this article, I will show you how to create an Azure Function to merge PDF files stored in SharePoint. The Function will be a generic service, which receives a list of file paths to merge. This means that you can trigger a request from SPFx, Power Automate, Logic Apps… Or anything else really. we are going to use the PFDsharp library, so our code will be super simple!
SPFx and modern SharePoint search page for searching the current location
Modern SharePoint sites on standard release will soon receive an update to add a search box to the top Office 365 bar. This allows you to search by default in the current location, like a site or a document library.

You can then configure the site to redirect user’s search queries to a custom search page and you can use the amazing PnP Modern Search web parts to create a great search experience
But what if you want to pass the context of the current location to the custom search page and keep searching only on that location by default?
SharePoint Framework application customizer
To pass the context of the current location to the custom SharePoint search page you can create an SPFx application customizer that you can add to the Top placeholder and use it to redirect the user to the search page, passing the context as a URL parameter.
An implementation example may look like this, adding a button below the search box , but you are free to decide how your component will look

Update your Application Customizer class to use the following function to identify the source Url
private _getSrcUrl = (): string => {
// set site url as default
let url: string = this.context.pageContext.web.absoluteUrl;
// if list or library and not Site Pages
if (this.context.pageContext.list
&& this.context.pageContext.list.serverRelativeUrl
&& this.context.pageContext.list.serverRelativeUrl.indexOf('SitePages') < 0) {
if (this.context.pageContext.web.serverRelativeUrl !== '/') {
// librarie's root folder
const listUrlPart = this.context.pageContext.list.serverRelativeUrl.replace(this.context.pageContext.web.serverRelativeUrl, '');
url += listUrlPart;
} else {
url += this.context.pageContext.list.serverRelativeUrl;
}
}
return url;
}
On the onClick event of the button, add the following code
const onClick = () => {
// this.props.srcUrl contains the value returned from the previous function that was passed to the React component as a property
let url = this.props.srcUrl && this.props.srcUrl !== '' ? this.props.srcUrl : window.location.href;
// get url parameters
const href: any = new URL(window.location.href);
// try to get relative path from id parameter - when exploring through views
let relativePath = href.searchParams.get("id");
if (!relativePath) {
// try to get relative path from RootFolder parameter - when accessing the folder via direct URL
relativePath = href.searchParams.get("RootFolder");
}
if (relativePath) {
// if inside a folder, build the url to include the full path to the folder
url = window.location.origin + encodeURIComponent(relativePath);
}
const w = window.open(`${customSearchPageUrl}?src=${url}`, '_blank').focus();
}
In summary, it generates a link to the search page and adds a src parameter with the current location. This needs to be done on the click event so that it always gets the correct location when exploring document library folders as the location is retrieved from the url on the document library (because of current issues related to this.context.application.navigatedEvent).
A sample url generated may look like this
https://contoso.sharepoint.com/sites/MySite/SitePages/Search.aspx?src=https://contoso.sharepoint.com%2Fsites%2FMySite%2FMyLibrary%2FMyFolder1%2FMyFolder2
Custom modern search page
I will not cover how to fully setup a custom search page using the PnP Modern Search web parts on this post. I will only cover how to use the location information available on the URL to limit the search scope.
To create search “zones”, we are going to use the Search Verticals web part. In the configuration, add two entries that use the same Result Source Identifier that contains all the results. Then on the “Current Location” vertical, set the query template to: {searchTerms} {?Path:”{QueryString.src}”}

The “Current Location” vertical will now only display results that have a Path value starting with the URL provided.
If you now test the solution (you can test just by adding different values to the src URL parameter) you will see that the results under “Current Location” are respecting the value provided.

The PnP Modern Search web parts are a great addition to your SharePoint sites. Not only they improve the search experience with the standard features, but they can only be configured to react to properties and configurations on your page.
Update metadata on SharePoint documents and folders
You have SharePoint managed metadata terms used in document libraries and you decide to update the label of a term. You can easily do so in the term store and wait for the change to take effect. All documents will display the new term label after the label change is propagated from the term store to the sites.
But what if you want to update a specific term, to a different term that already exists in the store? If you only have a small number of documents and folders that need to be updated, you can simply do the update manually. But if you need to update a large number of items, unfortunately this is a scenario that will require some automation.
The following script will to the job for you.
Summary
- PnP PowerShell for all the interactions with SharePoint
- Uses search to find all the documents/folders
- Checks the current value of the list item, in case search index is not updated
- Handle single or multi values
- Performs a system update: Modified and Modified by are not updated
- Generates a CSV file with the items parsed
The script currently connects to a specific site and assumes that all items retrieved from search belong to the site as this was the scenario tested. If you need to connect to multiple sites, you may need to adjust the script slightly to handle that.
Requirements
- PnP PowerShell available on the machine running the script
- A SharePoint search managed property mapped to the custom field associated with the term set (for example, RefinableString01)
Script to update metadata
$siteUrl = "https://contoso.sharepoint.com/sites/Home"
# ampersand: &
$oldTerm = "My old term label" -replace '&', '&'
$newTerm = "My new term label" -replace '&', '&'
$listColumn = "MyCustomField"
$termPath = "My Term Group|My Term Set|"
$Query = "* RefinableString01=""$oldTerm"""
$LogFile = "C:\users\$env:USERNAME\Desktop\UpdatedItems.csv"
# ---------------------------------
Connect-PnPOnline -Url $siteUrl -UseWebLogin
$SearchResults = Submit-PnPSearchQuery -Query $query -All -TrimDuplicates $false -SelectProperties ListItemID, ContentTypeId
# $SearchResults
$results = @()
foreach ($ResultRow in $SearchResults.ResultRows) {
$itemId = $ResultRow["ListItemID"]
$library = $ResultRow["ParentLink"].Split("/")[5] # quick way to get library from path
$contentTypeId = $ResultRow["ContentTypeId"]
$path = $ResultRow["Path"]
$parentLink = $ResultRow["ParentLink"]
$type = ""
Write-Host "Path: $path"
if ($contentTypeId -like '0x0101*') {
Write-Host "Document" -ForegroundColor Yellow
$type = 'Document'
}
if ($contentTypeId -like '0x0120*') {
Write-Host "Folder" -ForegroundColor Yellow
$type = 'Folder'
}
# Get list item
$listItem = Get-PnPListItem -List $library -Id $itemId -Fields $listColumn
if ($null -ne $listItem[$listColumn]) {
# Generate new value for the field
$termsWithPath = $null
if ($listItem[$listColumn].Count -gt 1) {
# check current value, in case search index is not updated
if ($listItem[$listColumn].Label -contains $oldTerm) {
# If multi-value, create an array of terms, and replace the old term by the new one
$termsWithPath = @()
foreach ($term in $listItem[$listColumn]) {
if ($term.Label -eq $oldTerm) {
$termsWithPath += $termPath + $newTerm
}
else {
$termsWithPath += $termPath + $term.Label
}
}
}
else {
Write-Host "Skipped: multi-value field does not contain term" -ForegroundColor Red
}
}
else {
# If single value, replace term
# check current value, in case search index is not updated
if ($listItem[$listColumn].Label -eq $oldTerm) {
$termsWithPath = $termPath + $newTerm
}
else {
Write-Host "Skipped: single-value field does not match term" -ForegroundColor Red
}
}
if ($null -ne $termsWithPath) {
# Update list item
$termsWithPath
Set-PnPListItem -List $library -Identity $itemId -SystemUpdate -Values @{"$listColumn" = $termsWithPath }
}
}
else {
Write-Host "Skipped: field is empty" -ForegroundColor Yellow
}
Write-Host "-------------" -ForegroundColor Yellow
#Creating object to export in .csv file
$results += [pscustomobject][ordered] @{
Library = $library
ItemId = $itemId
Type = $type
ParentLink = $parentLink
Path = $path
}
# break
}
$results | Export-Csv -Path $LogFile -NoTypeInformation
SharePoint folder filter SPFx extension
In modern SharePoint libraries containing large collections of folders, it may be difficult to navigate your way around the folder hierarchy. The library loads batches of 30 folders as you scroll down the list, making it difficult to find a specific item.
Would it not be great if you could easily filter the collection of folders?
You can try to use search to find the desired item quicker, but if you have content with similar names, the suggested results are not always relevant.
I built a super simple SharePoint Framework list extension to filter folders and address this limitation. Check the video below to see the SharePoint folder filter extension in action.
SharePoint folder filter extension demo
I have deployed the extension to all sites in a client tenant. The feedback from end users was amazing!
I have extracted the functionality to explore folders from the solution and created a reusable control. The control was submitted to the PnP reusable controls project and will hopefully be available soon for anyone to use.
Update: the FolderExplorer control is now available within the PnP repository on GitHub.
The folder filter extension is very simple. It only needs to control the visibility of the side panel and redirect the user to the selected folder.
The extension is only visible for document libraries. It can be deployed globally to the tenant app catalog and be made available to all sites.
Super simple and a great time saver for end users!
I am also planning to release the extension as a sample to the PnPextension samples repository.
Update: a sample solution is now available within the PnP extensions samples repository on GitHub.
I have now also done a demo of the solution on the SharePoint developer community call
Enjoy!
Reset API access in SharePoint online for SPFx solutions
If you try to approve API access requests in SharePoint online and get a generic error like the one below, just remember the first rule of IT: “turn it off and on again”
Continue reading “Reset API access in SharePoint online for SPFx solutions”Convert PnP TaxonomyPicker selection to update value
I am a big fan of the PnP reusable controls and previously delivered some sessions about them. You can find the slides for one session on this blog post. One of my favorite controls is the TaxonomyPicker control, which I often use in custom forms to update list columns.
When using the PnP TaxonomyPicker reusable control to let the user select values for a managed metadata list field in SharePoint, you have to convert that selection into an object that you can then pass to the REST api when updating the field value.
Continue reading “Convert PnP TaxonomyPicker selection to update value”Copy & move SharePoint documents/folders using PnPjs
The latest release of PnPjs contains 4 new methods. They allow you to copy and move, files and folders, to a different folder on the same or a different site collection. And they are incredibly fast!
Motivation
I have been recently asked by a client to develop a feature replacement to the default SharePoint copy and move to features. The OOB versions had some limitations that were causing problems to end users. With this in mind, I started by creating a proof-of-concept solution (SPFx list view command set extension). But soon realized that the functionality to copy and move files/folders between sites was not available in PnPjs. And so I added it and submitted a pull request (my first ever for the main PnPjs repository)!
Update: I wrote a blog post providing more details about the copy and move extension. You can read more here.
New copy and move features
- Copy and move documents and folders to the same site. PnPjs already had methods to do this, but using a different API endpoint
- Copy and move documents and folders to a different site
- If a file with the same name already exists, you can decide to replace or keep both. When keeping both files, a numeric value will by added to the name of the new file
- If a folder with the same name already exists when moving, you can decide to keep both. When keeping both folders, a numeric value will by added to the name of the new folder. The replace option is not available for folders
- Copying will not persist the version history of the source file
- Moving will persist the version history of the source file
Usage
As part of the pull request to add the new capabilities I have also updated the documentation (and tests) so people can easily understand how to use them. In short, your code will look similar to this (or use promises if you prefer):
Copy file by path
await sp.web.getFileByServerRelativePath(srcPath).copyByPath(`${destPath}/${name}`, shouldOverWrite, KeepBoth);
Move file by path
await sp.web.getFileByServerRelativePath(srcPath).moveByPath(`${destPath}/${name}`, shouldOverWrite, KeepBoth);
Copy folder by path
await sp.web.getFolderByServerRelativePath(srcPath).copyByPath(`${destPath}/${name}`, keepBoth);
Move folder by path
await sp.web.getFolderByServerRelativePath(srcPath).moveByPath(`${destPath}/${name}`, keepBoth);
Extra: Contributing to PnPjs
I use PnPjs for a very long time as it immensely helps me on my daily job, so I’m very happy that I was able to add a little bit more to it.
The project is so big that the first feeling I had was that adding the code there would take me longer than doing it on my own solution – but it clearly was the right thing to do. But guess what? The code is so well structured that this is actually pretty simple! All I had to do was to find the file where my functions should go (just follow the logical structure of folders that mirror the different packages) and find a similar function that I used as a starting point. Update the reference to the target REST API endpoint, update the data passed in the body (for POST requests) and all was done! All that was left to do after that was to update the documentation and tests, where I followed a similar approach.
We can all use it on any project going forward without having to worry about it! Sharing is caring 🙂
Delete all SharePoint list items with PowerShell
Did you ever wanted to quickly delete all items from a SharePoint list without having to run into complex scenarios?
This can be simply achieved using PnP Powershell (obviously!). And it fits in a single line!
If you are looking for options to delete documents from SharePoint document libraries, maybe this post can help.
Delete SharePoint list items
Connect to the site using Connect-PnPOnline and then run:
Get-PnPList -Identity Lists/MyList | Get-PnPListItem -PageSize 100 -ScriptBlock { Param($items)
$items.Context.ExecuteQuery() } | % {$_.DeleteObject()}