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 our 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”.
UPDATE: Unfortunately the images for this post were lost when I had to migrate the contents of the blog. Hope things still make sense as you read through.
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”
Limitations of default features
The following points were the most significant limitations behind the decision to create the new feature:
- Slow performance when displaying a 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 a 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 in 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).
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.
A simple filter is available at the top of the list to filter the data array based on user input.
At the bottom, a custom control lets the user create a new folder if he wishes to do so (similar to the OOB feature)
Progress panel
Once the user selects a target folder using the “Copy here” button, the interface replaces the list of folders with a progress panel.
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 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.