File created or modified in SharePoint – broken trigger

file created or modified

You can find many problems when using Power Automate and Azure Logic Apps with complex SharePoint lists. This article will cover another one that cause triggers to break. If you use the “When a file is created or modified in SharePoint” trigger, keep reading.

Continue reading “File created or modified in SharePoint – broken trigger”

No-code modern SharePoint site redirect

News link

In classic SharePoint sites, it was possible to redirect users to a different site using a few different approaches. But modern SharePoint sites are more restrictive (for good) and offer different features that require different approaches.
Fortunately, if you need to redirect users when they try to access the homepage of a specific site, this can be easily achieved using a simple no-code solution.

Continue reading “No-code modern SharePoint site redirect”

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.

modern SharePoint search
Top search box

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

advanced modern SharePoint search

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}”}

Search Verticals configuration

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

Trigger When a file is created or modified in SharePoint

Power Automate and Azure Logic Apps are great to use as automation tools for processes that include SharePoint data. A trigger that I often use is for when a file is created or modified in SharePoint. Unfortunately, they also seem to suffer from some limitations on large lists/libraries which can be quite hard to troubleshoot.
On this blog post, I describe the issue that I have recently experienced and how it can be quickly resolved.

Problem with flow – file created or modified

I encountered an issue on a client Office 365 tenant where flow triggers from a specific list were frequently breaking. The trigger used was When a file is created or modified (properties only) and seemed to only fail on that particular list. The list had around 450k documents at the time and multiple custom fields.
A support call was going on for about 3 weeks and during that period, the only workaround to keep functionality was to create a copy of the broken flow by using the “Save as” option. This would keep us running for a few days until the copy was also broken and we would create a new temporary copy – far from ideal, but kept things running.

Azure Logic Apps

Trying to find a solution to the issue or a reliable long term alternative, I went to re-create the flow as an Azure Logic App. Configured the logic app to run every minute and tried to test it by uploading some documents to the library.
Nothing happened.
I started troubleshooting and 1 hour later the instances were executed without any change being made. As I kept uploading documents for testing, the logic app kept running on significantly delayed schedules.

Solution

The solution turned out to be quite simple.
I tried to test different triggers and created a new logic app using the When a file is created (properties only) trigger. When testing it the first time, it failed, but game me the solution to the problem. The error message stated that the Created column was being indexed on the list and that the logic app would start working shortly. I went to the Indexed Columns and could see a label next to the Created column to show that it was being indexed.

In my case, the Modified field was already (automatically) indexed on the list, so after the index of the Created field completed, both logic apps started working as expected.

In summary, if you have large SharePoint lists and need to use automation triggers for when items are created or modified, double-check that the relevant list columns are indexed!
The link to Indexed Columns page for each list can be accessed under list Settings, at the bottom of the Columns section (just before the Views section).

Send items to second-stage SharePoint recycle bin

SharePoint recycle bin

When a document or folder is deleted in SharePoint, it goes to the first-stage (site) recycle bin. After some time, it is moved to the second-stage recycle bin. And it stays there for another period before being completely deleted.
If you need to delete a very large number of documents in SharePoint, you may consider sending the items straight to the second-level recycle bin. This will prevent “flooding” the first-stage recycle bin with items, which would make the recycle bin hard to use for manually restoring deleted items.

This can be easily done with a few lines of PnP PowerShell.

First we need to delete the item and send it to the first-level recycle bin. We then query the recycle bin and retrieve the item with a DirName and Title that match the item previously deleted.

# Use Get-PnPListItem to retrieve the item(s) to delete
Remove-PnPListItem -List $DocLib -Identity $Item["ID"] -Recycle -Force
$path = $Item["FileRef"].substring(1) # remove first '/' from path as recycle bin items don't start with '/'
$path = $path.substring(0, $path.LastIndexOf('/')) # remove folder name and last '/'
$deletedItem = Get-PnPRecycleBinItem -FirstStage -RowLimit 1 | Where-Object { $_.DirName -Eq $path -and $_.Title -Eq $Item["FileLeafRef"]}
Move-PnpRecycleBinItem -Identity "$($deletedItem.Id)" -Force

Sample use case: Send all empty folders and files to second-stage recycle bin

The following script is an extension of the script that Veronique Lengelle published on her blog. The original script was to find empty folders and documents on a site. I have extended it to send all empty folders and files to the second-stage recycle bin. Finally, we move that item to the second-level recycle bin.

#Connect to SPO
Connect-PnPOnline -Url https://contoso.sharepoint.com/sites/Test -UseWebLogin
#Store in variable all the document libraries in the site
$DocLibrary = Get-PnPList | Where-Object { $_.BaseTemplate -eq 101 } 
$LogFile = "C:\users\$env:USERNAME\Desktop\SPOEmptyFoldersAndDocuments.csv"
$results = @()
foreach ($DocLib in $DocLibrary) {
    #Get list of all folders and documents in the document library
    $AllItems = Get-PnPListItem -PageSize 1000 -List $DocLib -Fields "SMTotalFileStreamSize", "Author", "ID"
    
    #Loop through each files/folders in the document library for folder size = 0
    foreach ($Item in $AllItems) {
        if ((([uint64]$Item["SMTotalFileStreamSize"]) -eq 0)) {
            Write-Host "Empty folder/file:" $Item["FileLeafRef"] -ForegroundColor Yellow
                
            #Creating object to export in .csv file
            $results += [pscustomobject][ordered] @{
                CreatedDate      = [DateTime]$Item["Created_x0020_Date"]
                FileName         = $Item["FileLeafRef"] 
                CreatedBy        = $Item.FieldValues.Author.LookupValue
                FilePath         = $Item["FileRef"]
                SizeInMB         = ($Item["SMTotalFileStreamSize"] / 1MB).ToString("N")
                LastModifiedBy   = $Item.FieldValues.Editor.LookupValue
                EditorEmail      = $Item.FieldValues.Editor.Email
                LastModifiedDate = [DateTime]$Item["Modified"]
            }
            #Remove item - send to first-level recycle bin
            Remove-PnPListItem -List $DocLib -Identity $Item["ID"] -Recycle -Force
            #Generate relative path in the same format as used by the recycle bin. Example: sites/Test/Shared Documents
            $path = $Item["FileRef"].substring(1) # remove first '/' from path as recycle bin items don't start with '/'
            $path = $path.substring(0, $path.LastIndexOf('/')) # remove folder name and last '/'
            #Get previously deleted item from first stage recycle bin using path and title
            $deletedItem = Get-PnPRecycleBinItem -FirstStage -RowLimit 1 | Where-Object { $_.DirName -Eq $path -and $_.Title -Eq $Item["FileLeafRef"]}
            #Move item to second-stage recycle bin
            Move-PnpRecycleBinItem -Identity "$($deletedItem.Id)" -Force
            
            Invoke-PnPQuery
        }#end of IF statement
    }
}
$results | Export-Csv -Path $LogFile -NoTypeInformation

SharePoint folder filter SPFx extension

Jump to folder

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

Jump to folder

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 PnP extension 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!

Convert PnP TaxonomyPicker selection to update value

TaxonomyPicker update

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”