Automatically create subfolders in SharePoint after record is saved in a model-driven App

If you need to automatically set up a SharePoint folder structure for your users in a model-driven app in real-time, it is easy to accomplish that with a few lines of code. Power Apps have a build-in integration for file storage inside of a SharePoint document library, if you enable document management on that table.

Showcase folder creation

When you initially access the documents subgrid a new folder is automatically created – accompanied by a record in SharePointDocumentLocation table. This record is the glue that ties the SharePoint folder to a specific record in Dataverse. The referenced record is saved in the RegardingObjectId.

The objective

If we want to automatically create a certain folder structure for our users to initially work with, we must set up this as a prerequisite before we can create any folders, since we do not know when users will access the subgrid for the first time. The upcoming solution uses the same request pattern that the platform is using for the OOB functionality of the document’s subgrid.

A Solution

Use the JavaScript function below in an onload event to create three folders once the record is initially saved. It will only trigger the folder creation once as we determine inside the function if folder creation is necessary.

Things to know

This is client-side code. It runs in the browser so there won't be any folder creation if records get created from workflows or via Web API. If that is an objective you need to revert to Server side logic in Power Automate or try to postpone folder creation until the point when the user is accessing the documents subgrid of the record.

namespace BlogSample { let isCreate = false; /** * Creates folders in default location on entity creation. Upon first load isCreate will be * initialized to true and after save with the subsequent onload the folders will be created. */ export async function createFolders(context: Xrm.Events.EventContext) { const fCtx = context.getFormContext(); if (fCtx.ui.getFormType() === XrmEnum.FormType.Create) { isCreate = true; console.log("first load.") return; } if (isCreate) { console.log("Need to create folders."); isCreate = false; const entityId = fCtx.data.entity.getId(); const entityLogicalName = fCtx.data.entity.getEntityName(); await retrieveSPDocuments(entityId, entityLogicalName); const foldersToCreate = ["FolderOne", "FolderTwo", "FolderThree"] for (const folder of foldersToCreate) { await createFolder(entityId, entityLogicalName, folder); } return; } console.log("nothing to do.") } /** * Retrieves elements in default sharepoint location or its subdirectory. * @param regardingObjectid The id of the record we want query documents for. * @param regardingObjectName The logical name of the record we want query documents for. * @param relativeLocation Queries for elements in a subdirectory. * @returns */ async function retrieveSPDocuments(regardingObjectid: string, regardingObjectName: string, relativeLocation: string = "") { let relLoc = `<filter type="and"> <condition attribute="relativelocation" operator="eq" value="${relativeLocation}"/> </filter>`; let query = `<fetch distinct="false" mapping="logical" returntotalrecordcount="true" no-lock="false"> <entity name="sharepointdocument"> <attribute name="documentid"/> <attribute name="fullname"/> <attribute name="relativelocation"/> <attribute name="filetype"/> <attribute name="absoluteurl"/> <attribute name="title"/> <attribute name="sharepointdocumentid"/> <order attribute="relativelocation" descending="false"/> <filter> <condition attribute="isrecursivefetch" operator="eq" value="0"/> </filter> ${(relativeLocation !== "") ? relLoc : ""} <link-entity name="${regardingObjectName}" from="${regardingObjectName}id" to="regardingobjectid" alias="bb"> <filter type="and"> <condition attribute="${regardingObjectName}id" operator="eq" value="${regardingObjectid}"/> </filter> </link-entity> </entity> </fetch>`; let spDocuments = await Xrm.WebApi.retrieveMultipleRecords("sharepointdocument", `?fetchXml=${encodeURIComponent(query)}`); return spDocuments; } /** * Create a folder for given entity record in default sharepoint location. * @param regardingObjectid The id of the record we want query documents for. * @param regardingObjectName The logical name of the record we want query documents for. * @param folderName The name of the new folder. * @param folderPath Create in subdirectory * @returns */ async function createFolder(regardingObjectid: string, regardingObjectName: string, folderName: string, folderPath: string = "") { console.log(`Create SP folder ${folderName} for ${regardingObjectName}: ${regardingObjectid}`); let spDocuments = await retrieveSPDocuments(regardingObjectid, regardingObjectName); const folderExists = spDocuments.entities.some((x: any) => x.fullname === folderName); if (folderExists) { console.log(`Folder '${folderName}' already exists!`); return spDocuments; } console.log(`Creating folder: ${folderName} in path: ${folderPath}`); let parentEntityRef: any = {}; parentEntityRef["@odata.type"] = `Microsoft.Dynamics.CRM.${regardingObjectName}`; parentEntityRef[`${regardingObjectName}id`] = `${regardingObjectid}`; let folderPayload = { "FileName": folderName, "LocationId": "00000000-0000-0000-0000-000000000000", "ParentEntityReference": parentEntityRef, "IsFolder": true, "FolderPath": folderPath }; await fetch(`${Xrm.Utility.getGlobalContext().getClientUrl()}/api/data/v${Xrm.Utility.getGlobalContext().getVersion().substring(0, 3)}/NewDocument`, { "headers": { "accept": "application/json", "content-type": "application/json", "odata-maxversion": "4.0", "odata-version": "4.0", "prefer": "odata.include-annotations=\"*\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin" }, "body": JSON.stringify(folderPayload), "method": 'POST', "mode": "cors", "credentials": "include" }); } }

Blog post written by

Marius Pothmann

Marius Pothmann is a Microsoft certified solution architect focusing on the Power Platform and cloud-native application development on Azure. Working on the Microsoft stack for over a decade gave him the opportunity to deep dive into many building blocks of successful enterprise application architectures like Dynamics 365, SQL Server, ADFS, ASP.NET (Core), SPA-based Web Apps, WPF/WinForms. If you think he can assist in solving your challenges drop us an e-mail and we will get back to you right away.