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 🙂

My path to receive the Microsoft MVP award

On the 1st of September, I opened my inbox and found a very  pleasant surprise. I had received the Microsoft MVP for Office Development award!

This post covers some of the things that led to the MVP award and also some personal thoughts about the MVP award program. Please note that this is only my own opinion, I don’t know the award criteria. Hope you find the post interesting.

Continue reading “My path to receive the Microsoft MVP award”

Copy all SharePoint terms in a term set to clipboard

copy terms

This is just one of those blog posts where there is no “rocket science” if you use the right tools. And the reason why I’m writing it is to just show how easy it is. All it takes is one line of PowerShell to copy all terms in a term set to the clipboard.
Of course there is a lot of “rocket science” here. But its inside PnP PowerShell and we don’t need to worry about it!

I use PnP PowerShell very often and I sometimes publish posts with useful scripts. You can find an example here to update metadata fields.

How many times have people asked you for a list of all the SharePoint managed metadata terms within a specific term set? They can simply access the term store, but let’s be honest, the user interface is not great at the moment…

Well, next time you get that request, simply connect to SharePoint using PnP PowerShell and then run the following command to copy all terms to the clipboard

Get-PnPTerm -TermSet "MyTermSet" -TermGroup "MyTermGroup" | Select Name | clip

Paste the result, for example, into an Excel file and the information is ready to be consumed.
Please note that the snippet above extracts a flat list of terms and does not include child terms.

Enjoy!

Enable modern document sets programmatically

document set

Some time ago, modern SharePoint sites received a new feature: modern Document Sets. In order to enable and use of this feature, “all” you have to do is enable the “Document Sets” feature under Site Collection Features. Then simply add the relevant content type to a library and you can start using them.

Simple right?
Well…perhaps not so simple if you try to do this programmatically.

All we have to do through the UI is enable a feature, so we expect the same when done programmatically. And here we have multiple options: CSOM, PowerShell, or perhaps a (PnP) provisioning template. But this “not always” works, as reported on GitHub at least here and here.
You can activate the feature and add the content type to the library, but you will get a file not found error for the document sets page (DocSetHome.aspx).

The first GitHub link above contains a workaround by Jlopean.
In summary, if Custom Scripts are not enabled for the site collection, things just don’t work, so ensure that you activate Custom Scripts (on the site collection!) before activating the Document Sets feature and all works fine.

If you looking for different options to enable Custom Scripts, here is a great blog post by Antti K. Koskela.

Many thanks to my colleague Alberto Suarez for helping investigate the issue and update the GitHub issues

Delete all SharePoint list items with PowerShell

delete items

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()}

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.

Setting managed metadata fields using PnP PowerShell

A few days ago, I was trying to set some managed metadata fields in a SharePoint library using the Set-PnPListItem command – which is greatly documented.
But having worked with SharePoint for quite a few years, I immediately noticed that the examples in documentation for managed metadata fields did not contain the exception case for terms with the ‘&’ character in the label. I had to deal with this case multiple times before, so writing this blog post to hopefully save some time in the future.

If you have taxonomy terms that use the ‘&’ character, behind the scenes it gets converted to ‘&’. Notice that the character looks different, because it is a different character. You can search online for information on this as there are plenty of blog posts explaining it in detail.

To handle the special character when setting the field with Set-PnPListItem, all you have to do is replace it. Then use the new string in the Set-PnPListItem command. This also works for fields with multiple values of course.

$myString = $myString -replace '&', '&'

I have also wrote a blog post with a script to replace a specific term across all documents and folders.

But be warned…

While I was working on this, I had multiple managed metadata fields to be set on the list. To avoid repetitive string replacements in my code, I created a very simple function to do that. The function received a string and simply returned the result after the replacement – super simple. This seemed to be fine and the output on the console was correct. I could even copy/paste the value and manually run the command to set the field myself. But it din’t work within the script. When using the variable with the return of the function, it didn’t work at all!

I suspect that somehow, returning the string from the function was messing up with the characters and something was going wrong (even though console output seemed correct), but never got to the bottom of it. Maybe it’s just a known thing on how PowerShell works that I don’t know…

If you have any idea, please leave a comment below 🙂 I would really like to understand what happened

SPFx web parts from different solutions on workbench

workbench customizer

Ever wondered how to add SharePoint Framework web parts from different solutions to your local workbench while developing on localhost? Then look no further 🙂

Even though this may not be a common requirement for everyone, there are cases where it could be handy to have different web parts running on the local workbench that belong to different solutions.
I was sitting on a plane without WiFi and was testing some things with a web part running locally. Was having some issues with the workbench page styles, so I tried to find a way to load the Workbench Customizer web part on the Workbench page, without adding it to my solution. It turned out to be fairly simple.

If you never heard about the Workbench Customizer web part, you can read more on this post.

Bundle the “external” solution

The first thing that you will have to do is go to the solution folder containing the web part that you want to load on the main solution and bundle it. Your solution needs to build for this to work. This will create the dist and lib folders, that will contain the files you need to copy to the other solution.
To bundle, we simply run the SharePoint Framework bundle task “gulp bundle

Copy the “external” solution to your main solution

Next, we need to copy some files from the “external” solution to the folders of your main solution. If your main solution does not have bin and dist folders, you can simply run “gulp bundle” to generate them.
All the files that we are going to copy are located in the dist and lib folders. The folders are locally generated and never included as part of your solution in your Git repository. Additionally, you can simply run the clean task (gulp clean) to remove everything.
In order for the web part to work, you will have to copy two folders:

  • dist – open the dist folder on the “external” solution and copy all the files to the dist folder of your main solution
  • lib>webparts>{your web part name} – open the lib>webparts folder on the “external” solution and copy the folder with the same name as your web part to the lib>webparts folder of your main solution. You should see different folders, displaying the different elements of your solution.

Serve your main solution

Everything is done and you can now run the main solution and add both web parts to the Workbench page. To run the main solution locally, simply run the task “gulp serve” as you would normally do.

You can add both web parts to the page and confirm that both work

Package your main solution

You can safely package your main solution while still having the files for the “external” solution on the dist and lib folders. Those will not be included on the package file 🙂

Cleaning up the main solution

If you want to remove all the files from the “external” solution on the main one, simply run “gulp clean”. It’s that easy.