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.

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”

SharePoint SPFx extension – Advanced copy and move

A client recently asked me to create an advanced version of the default “Copy to” and “Move to” SharePoint capabilities available on every document library. This blog post will cover the main decisions, challenges and tools that I used to achieve this.

After out client went live with a new SharePoint site to be used as the main “landing page” for the company, they started receiving some feedback from end users. I created some custom SPFx web parts and extensions for the site, so was expecting some feedback on my work. Instead, the most common feature that users were providing feedback on was the out-of-the-box “Copy to” and “Move to”.

I explained to the client that it would not be possible to customise or disable out-of-the-box features, so they decided to create alternative versions: “Advanced copy” and “Advanced move”

Advanced copy

Limitations of default features

The following points were the most significant limitations behind the decision to create the new feature:

  • Slow performance when displaying large number of folders
  • No option to filter/search a large list of folders
  • No target location set by default
  • Limited space to display folder names

Requirements

Based on the user feedback and limitations identified during testing, we defined the following key requirements:

  • Look and feel should not be very different from out-of-the-box features
  • It should provide better performance, mainly when loading and displaying a large number of folders
  • Provide an option to filter a list of folders by name
  • It should use the current folder as the default location. Users were often copying/moving documents to folders related to the current location
  • Before completing the operation, the user should be able to specify new names for the documents or folders

Implementation

While I’m working on getting permission from my client to publish the solution to the open source PnP community repositories, so that others facing similar challenges can benefit from it, I decided to publish this information that may help others on a similar journey.

I implemented the solution as a custom SPFx list view command set with two options that share the same code base. Depending on the option selected, the relevant action (copy/move) label is displayed on the screen. The appropriate service function is then called, but other than that, all the other code is shared.

SharePoint API calls

I have used PnPjs for all the SharePoint REST API calls.
Copy document, move document, copy folder and move folder options use new capabilities that I added to PnPjs for this purpose. You can read more about it on a separate blog post that I published recently.

Performance was never an issue. And often, large collections of folders are loaded multiple times faster than the out-of-the-box counterparts.

// get list of libraries within a site:
await sp.site.getDocumentLibraries(webAbsoluteUrl)
// get list of folders within a folder/library
await web.getFolderByServerRelativeUrl(folderRelativeUrl).folders.select('Name', 'ServerRelativeUrl').orderBy('Name').get()
// add new folder
await web.getFolderByServerRelativeUrl(folderRelativeUrl).folders.add(name)
// copy file
await sp.web.getFileByServerRelativePath(srcPath).copyByPath(`${destPath}/${name}`, shouldOverWrite, KeepBoth);
// move file
await sp.web.getFileByServerRelativePath(srcPath).moveByPath(`${destPath}/${name}`, shouldOverWrite, KeepBoth);
// copy folder
await sp.web.getFolderByServerRelativePath(srcPath).copyByPath(`${destPath}/${name}`, keepBoth);
// move folder
await sp.web.getFolderByServerRelativePath(srcPath).moveByPath(`${destPath}/${name}`, keepBoth);

Main User Interface components

Breadcrumb

A breadcrumb component is available at the top of the panel to allow the user to navigate to a previous folder within the hierarchy level. It displays a node for each section of the path to the folder currently selected and an option to select a different “place” – sites or OneDrive (in the future).

Breadcrumb

The breadcrumb is based on the Breadcrumb control from Office UI Fabric.

<Breadcrumb items={breadCrumbItems} className={styles.breadcrumbPath} maxDisplayedItems={3} overflowIndex={overflowIndex} />

And the function to generate the array of breadcrumb items. Each item has an onClick callback function to get the list of sub-folders for that path.

  /**
   * Get breadcrumb items
   * @returns an array of IBreadcrumbItem objects
   */
  private _getCurrentBreadcrumbItems = (): IBreadcrumbItem[] => {
    let items: IBreadcrumbItem[] = [];
    let rootItem: IBreadcrumbItem = { text: 'Places', key: 'Places', onClick: this._showPlacePicker, };
    items.push(rootItem);
    let siteItem: IBreadcrumbItem = { text: this.state.selectedSite.Title, key: 'Site', onClick: this._getSiteLibraries.bind(this, this.state.selectedSite) };
    items.push(siteItem);
    if (this.state.selectedLibrary != null) {
      let libraryItem: IBreadcrumbItem = { text: this.state.selectedLibrary.Title, key: 'Library', onClick: this._getRootFolders.bind(this, this.state.selectedLibrary) };
      items.push(libraryItem);
    }
    if (this.state.selectedFolder) {
      const folderPathSplit = this.state.selectedFolder.ServerRelativeUrl.replace(this.state.selectedLibrary.ServerRelativeUrl + '/', '').split('/');
      let folderPath = this.state.selectedLibrary.ServerRelativeUrl;
      folderPathSplit.forEach((folderName, index) => {
        folderPath += '/' + folderName;
        let folderItem: IBreadcrumbItem = { text: folderName, key: `Folder-${index.toString()}`, onClick: this._getSubFolders.bind(this, { Name: folderName, ServerRelativeUrl: folderPath }) };
        items.push(folderItem);
      });
    }
    items[items.length - 1].isCurrentItem = true;
    return items;
  }
Folders list

This is the core component. It lists the sub-folders currently available under the current path (either a library or a folder) and allows the user to click on one of the sub-folders to move one level deep into the hierarchy.

Folders list

A simple filter is available at the top of the list to filter the data array based on user input.

Filter

At the bottom, a custom control lets the user create a new folder if he wishes to do so (similar to the OOB feature)

Folder creation
Progress panel

Once the user selects a target folder using the “Copy here” button, the interface replaces the list of folders by a progress panel.

Selecting destination
Progress panel
Progress panel with options to replace or keep both

An array stores each file/folder selected by the user. Each object of the array has a “status” property that controls the displayed status for each item on the screen.
For example, if the data service reports that the file already exists on the target folder, the corresponding array item is updated accordingly. A friendly message is then displayed to the user, next to that item, with buttons to replace or keep both files.

Limitations

It’s not possible (at the least at the moment?) to disable the out-of-the-box “Copy to” and “Move to” capabilities. This leads to “duplicated functionality” for end users that need to be trained on what feature to use. It would be great if it was possible for administrators of a site to disable specific default features.

Copy & move SharePoint documents/folders using PnPjs

copy move

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 🙂

Get list of recent documents in SharePoint

recent documents

Similar to my last post Get list of frequent sites in SharePoint , this time I’m using the same approach to query a different API and get the recent documents for the current user.

SharePoint offers an OOB web part that you can use to list the recent documents for the current user. But what if you need the exact same information for a custom SharePoint Framework solution?

WARNING

Unfortunately, it seems that this is not currently possible using the SharePoint REST API or MS Graph. The API used on the sample code below is currently not documented and you should understand the risks when using it!

Hopefully Microsoft will make this information available soon via MS Graph or document this API.

Recent documents

[4th Nov 2019 – Update – Added new required HTTP headers when requesting data from the service: X-Office-Platform, X-Office-Application, X-Office-Version]

You can also get the code from this gist

// within your web part onInit method, get the request digest token
const digestCache: IDigestCache = this.context.serviceScope.consume(DigestCache.serviceKey);
const requestDigest = await digestCache.fetchDigest(this.context.pageContext.web.serverRelativeUrl);


//now on your data access layer
// get token
const authRequestHeaders: Headers = new Headers();
authRequestHeaders.append("Accept", "application/json;odata.metadata=minimal");
authRequestHeaders.append("Content-Type", "application/json; charset=utf-8");
authRequestHeaders.append("odata-version", "4.0");
authRequestHeaders.append("x-requestdigest", this._requestDigest);

var resourceData = {
  "resource": "https://officeapps.live.com",
};

const authRequestOptions: IHttpClientOptions = {
  headers: authRequestHeaders,
  body: JSON.stringify(resourceData),
};

const authEndpointUrl = this._webAbsoluteUrl + '/_api/SP.OAuth.Token/Acquire';
const authRawResponse = await this._httpClient.post(authEndpointUrl, HttpClient.configurations.v1, authRequestOptions);
const auth = await authRawResponse.json();

// get data
const dataRequestHeaders: Headers = new Headers();
dataRequestHeaders.append("Authorization", "Bearer " + auth.access_token);
dataRequestHeaders.append("Content-Type", "application/json");
dataRequestHeaders.append("X-Office-Platform", "Web");
dataRequestHeaders.append("X-Office-Application", "120");
dataRequestHeaders.append("X-Office-Version", "*");

const dataRequestOptions: IHttpClientOptions = {
  headers: dataRequestHeaders,
};

const dataEndpointUrl = `https://ocws.officeapps.live.com/ocs/v2/recent/docs?apps=Word,Excel,PowerPoint,Visio,OneNote,Sway,PdfViewer&show=${count}&sort=Date`;
const dataRawResponse = await this._httpClient.get(dataEndpointUrl, HttpClient.configurations.v1, dataRequestOptions);
const data = await dataRawResponse.json();

Get list of frequent sites in SharePoint

frequent sites

SharePoint offers an OOB web part that you can use to list the frequent sites for the current user. But what if you need the exact same information for a custom SharePoint Framework solution?

I have also wrote a similar blog post on how to retrieve recent documents for current user. You can read more here.

WARNING

Unfortunately, it seems that this is not currently possible using the SharePoint REST API or MS Graph. The API used on the sample code below is currently not documented and you should understand the risks when using it!

Some related threads:

https://github.com/SharePoint/sp-dev-docs/issues/1689

https://sharepoint.uservoice.com/forums/329220-sharepoint-dev-platform/suggestions/34075903-api-support-for-followed-sties

Please vote on uservoice 🙂 hopefully Microsoft will make this information available soon via MS Graph or document this API.

Frequent sites

You can get the code from this gist

// get data endpoints, token and payload
const contextRequestHeaders: Headers = new Headers();
contextRequestHeaders.append("Accept", "application/json;odata.metadata=minimal");
contextRequestHeaders.append("odata-version", "4.0");

const contextRequestOptions: IHttpClientOptions = {
  headers: contextRequestHeaders,
};

const contextEndpointUrl = this._webAbsoluteUrl + '/_api/sphomeservice/context?$expand=Token,Payload';
const contextRawResponse = await this._httpClient.get(contextEndpointUrl, HttpClient.configurations.v1, contextRequestOptions);
const context = await contextRawResponse.json();

// get data
const dataRequestHeaders: Headers = new Headers();
dataRequestHeaders.append("Authorization", "Bearer " + context.Token.access_token);
dataRequestHeaders.append("sphome-apicontext", context.Payload);

const dataRequestOptions: IHttpClientOptions = {
  headers: dataRequestHeaders,
};

const dataEndpointUrl = context.Urls[0] + `/api/v1/sites/feed?start=0&count=${count}&acronyms=true`;
const dataRawResponse = await this._httpClient.get(dataEndpointUrl, HttpClient.configurations.v1, dataRequestOptions);
const data = await dataRawResponse.json();

Office UI Fabric images for SPfx projects

office ui fabric icons

When creating SPFx solutions, you will sometimes require base64-encoded images. A common scenario is when you create a ListView Command Set extension . And then you look at Office UI Fabric Icons and think how nice it would be if you could easily get the images as base64-encoded strings to use on SPFx solutions.

And today I found a great tool to do this!

While searching for a tool/website to convert the icons, I stumbled across this amazing blog post: “I Made a Tool to Generate Images Using Office UI Fabric Icons

And on the post, there’s the link to the great tool on codepen.

In this example, I created an icon for a SPFx ListView Command Set that uses 16×16 px icons from Fabric React.

As you can see from the image, all you have to do is copy the Data URL field and paste that value on the manifest.json file of your solution.

The icon will look great on the page (search folders):

Unfortunately the tool seems to have a limitation and it doesn’t give you the option to have transparent background, so be aware when selecting a background colour.
Update: I am glad to confirm that I was wrong and the tool does in fact support transparent backgrounds! Just use ‘transparent’ as the value for the background colour as per the image above.

Hope you find this helpful.