Using JavaScript and Cloud Flows to download files from a SharePoint document library in Power Pages

Even though we can now use Virtual Tables to expose SharePoint lists in a Power Pages site, the same does not apply to SharePoint Document libraries.

You can use the native SharePoint integration to display content that was set up in that way from the Model Driven Apps side, but you cannot expose a custom library in that way.

For example, if you want to expose a random library with Document templates for users to download, there is no simple way by using out-of-the-box features.

However, you can use the new Power Automate integration to leverage that, by creating a custom Page and manipulating data via JavaScript as in the sample below:

Overview of the approach

In order for this integration to work, we will need:

  1. A SharePoint site with a library for that purpose.
  2. A Power Automate Flow that lists a library/folder files
  3. Another Power Automate Flow to retrieve file contents
  4. And finally a custom page with JavaScript code to manipulate the flows and results

Note: This post showcases the integration in a demo scenario, more error handling, SharePoint thresholds handling and other aspects might be needed for production scenarios (for example, using environment variables, handling ALM etc).

Important note for production use: This sample was meant to be used in cases where documents are meant to be public/and as a demonstration of the integration capabilities. When implementing features for production, it is crucial to ensure the security of user data. Please be diligent in adding security checks for user authentication and authorization to prevent any unauthorized access, document downloads or data breaches.

Licensing Requirements: You can use any Power Automate license, but for production instances, Microsoft recommends going with the Power Automate per flow license. Check out the pre-requisites section on this Microsoft article for details on licensing requirements: Configure – Cloud Flows integration – Prerequisites

Create the SharePoint site & upload the documents

Create a SharePoint site in the format/template you wish. I recommend creating one only for that purpose of holding the document templates library, so there is no need to worry too much with permissions, as the Power Automate flow will receive folders/files identifiers as by path (which could expose any file in the site – but if the site is used only to store those templates, that is fine).

Create the Power Automate flow to list files in a library/folder

In Power Pages studio, go to the Set up workspace and navigate to App integrations, where you will find Cloud flows. Click on the “+ Create new flow” button.

In the flow creation interface, search for Power Pages and locate and select the “When Power Pages calls a flow” trigger to initiate the integration.

Then add the following actions setup:

  1. Flow name: Power Pages – List SharePoint folder contents
  2. Trigger: Power Pages – When Power Pages calls a Flow
    • Add a text input named Folder: This will be used to navigate the libraries folders
  3. Action: SharePoint – Get Files (properties only)
    • Select the site and library you created previously
    • In the ‘Limit entries to folder’, use the ‘Folder’ parameter from the trigger
  4. Action: Return value(s) to Power Pages
    • Add an output parameter named ‘result‘, with the value of the Body of the outputs from the SharePoint action

Create the Power Automate flow to retrieve file contents

Create another flow as above, with the same trigger but following setup:

  1. Flow Name: Power Pages – Get SharePoint File Content as Base64
  2. Trigger: Power Pages – When Power Pages calls a Flow
    • Add a text input named FileId: This will be used to load specific file content
  3. Action: SharePoint – Get File Content
    • Select the site you created previously
    • In the parameter ‘File Identifier’, use the ‘FileId’ parameter from the trigger
    • In the ‘Infer Content Type’ parameter, select ‘No’. This will make the file contents always be available as Base 64 in this action’s response.
  4. Action: Return value(s) to Power Pages
    • Add an output parameter named ‘FileContents‘, with the following formula as value: body(‘Get_file_content’)?[‘$content’]

Add the flows to the site

Back to Power Pages studio, in the Cloud Flows section, click on Add Existing Cloud flow. Search for “Power Pages – Get SharePoint File Content as Base64” and select it. In the roles section, add the roles you want to have access to the flow. Copy the URL generated (you will need this in the Javascript code).

It’s also important to assign only the appropriate Web Roles to your flow by editing its properties. This ensures the flow will be called only by the users that belong to the Web Roles you want (and not all authenticated users in the site). This is crucial if you want to restrict usage of the flows for security.

Repeat the same steps above for the flow “Power Pages – List SharePoint folder contents“.

You should now see the flows under your site’s flows list:

Create the custom page and add the custom code

In Pages workspace and choose “+ Page” to create a new page. Name the page “Document Templates.” To customize the page , select “Edit code” to open Visual Studio Code.

In the code editor for the HTML content of the page:

Paste the code below:

<div class="row" style="min-height: auto; padding: 8px;">
  <div class="container" style="display: flex; flex-wrap: wrap;">
    <div class="spbreadcrumb" id="divFolderBreadcrumbContainer"></div>
    <div class="spfoldercontent">   
      <div id="table_container"></div>
      <div class="spinner" id="divSpinner">
        <div class="spinner-image"></div>
        <span id="spnSpinnerMessage"></span>
      </div>
    </div>
  </div>
</div>
<style> 
  .spinner {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(255, 255, 255, 0.8);
    justify-content: center;
    align-items: center;
    flex-direction: column;
  }

  .spbreadcrumb {
    margin-bottom: 16px;
    width: 100%;
  }

  .spfoldercontent {
    min-width: 360px;
    width: 100%;
  }

  .spfoldercontent button {
    border: none;
    background-color: transparent;
    display: flex;
    gap: 8px;
    flex-direction: row;
    align-items: center;
  }

  .spfoldercontent td:first-child {
    min-width: 300px;
  }

  .spinner-image {    
    width: 75px;
    height: 75px;
    animation: spinnerwheel 1.5s linear infinite;
    border: 4px solid #c4c0c0;
    border-top: 4px solid #0580ab;
    border-radius: 50%;
  }

  @keyframes spinnerwheel {
    0% {
      transform: rotate(0deg);
    }

    100% {
      transform: rotate(360deg);
    }
  }
</style>
<script>
  var _getFileAsBase64FlowUrl = "https://yoursite.powerappsportals.com/_api/cloudflow/v1.0/trigger/<flow id>";
  var _getFolderContentFlowUrl = "https://yoursite.powerappsportals.com/_api/cloudflow/v1.0/trigger/<flow id>";

  document.addEventListener("DOMContentLoaded", () => {
    LoadSharePointFolderContents();
  });

  function ShowError(message) {
    alert(message);   
  } 

  function ShowSpinner(message) {
    document.getElementById('spnSpinnerMessage').textContent = message;
    document.getElementById('divSpinner').style.display = 'flex';
  }
  
  function HideSpinner() {
    document.getElementById('divSpinner').style.display = 'none';
  }

  function GenerateBreadcrumb(folderPath) {
    const folders = folderPath.split('/').filter(folder => folder.trim() !== '');

    let breadcrumbHtml = '';
    let currentFolderPath = '';

    for (let i = 0; i < folders.length; i++) {
      const folder = folders[i];
      currentFolderPath += `/${folder}`;
      if (i < folders.length - 1) {
        breadcrumbHtml += `<a href="#" onclick="LoadSharePointFolderContents('${currentFolderPath}')">${folder}</a> / `;
      } else {
        //Last item in path should not be clickable - keeping as link just to use basic styles
        breadcrumbHtml += `<a style="text-decoration: none;"">${folder}</a>`;
      }
    }
    document.getElementById('divFolderBreadcrumbContainer').innerHTML = breadcrumbHtml;
  }

  function LoadSharePointFolderContents(folder) {
    ShowSpinner("Loading folder contents...");
    document.getElementById("table_container").innerHTML = "";
    var payload = {};
    var data = {};
    folder = folder ?? "";
    data["Folder"] = folder;

    payload.eventData = JSON.stringify(data);

    shell.ajaxSafePost({
      type: "POST",
      contentType: "application/json",
      url: _getFolderContentFlowUrl,
      processData: false,
      data: JSON.stringify(payload),
      global: false,
    })
      .done(function (response) {
        let fileList = JSON.parse(JSON.parse(response).result);
        console.log(JSON.parse(response))
        console.log(fileList);
        GenerateSharePointTable(fileList.value);
        if (!folder && fileList.value.length > 0) {
          folder = fileList.value[0]["{FullPath}"].split("/")[0];
        }
        GenerateBreadcrumb(folder);
        HideSpinner();
      })
      .fail(function () {
        HideSpinner();
        ShowError("Error loading folder content");
      })
  }

  function OpenSharePointDocument(FileId, FileName) {
    ShowSpinner("Downloading file...")

    var payload = {};
    var data = {};
    data["FileId"] = FileId;

    payload.eventData = JSON.stringify(data);

    shell.ajaxSafePost({
      type: "POST",
      contentType: "application/json",
      url: _getFileAsBase64FlowUrl,
      processData: false,
      data: JSON.stringify(payload),
      global: false,
    })
      .done(function (response) {
        let fileData = JSON.parse(response);
        DownloadBase64File(fileData.filecontents, FileName);
        HideSpinner();
      })
      .fail(function () {
        HideSpinner();
        ShowError("Error loading file content");
      })
  }
  //Generates a dummy link auto downloading the file when the function is called
  function DownloadBase64File(base64FileContent, fileName) {
    var base64String = "data:application/octet-stream;base64," + base64FileContent;
    const downloadLink = document.createElement("a");
    downloadLink.href = base64String;
    downloadLink.download = fileName;
    downloadLink.click();
  }

  function GenerateSharePointTable(jsonData) {
    let container = document.getElementById("table_container");
    container.innerHTML = "";
    let table = document.createElement("table");
    if (jsonData.length > 0) {
      let thead = document.createElement("thead");
      let tr = document.createElement("tr");
      let th = document.createElement("th");

      th.innerText = "Name";
      tr.appendChild(th);
      thead.appendChild(tr);
      table.append(tr)

      jsonData.forEach((item) => {
        let tr = document.createElement("tr");
       
        let spanIcon = document.createElement("span");
        let spanText = document.createElement("span");

        let td = document.createElement("td");
        td.innerText = item["{FilenameWithExtension}"];
        tr.appendChild(td);
        let td_open = document.createElement("td");
        var btn = document.createElement('button');
        
        spanIcon.classList.add("glyphicon");
        if (item["{IsFolder}"] == true) {
          //for folders, we reload the contents with files for this folder instead of the root folder
          btn.onclick = function () { LoadSharePointFolderContents(item["{FullPath}"]); }          
          spanText.innerText = "Open Folder";
          spanIcon.classList.add("glyphicon-folder-open");
        }
        else {
          btn.onclick = function () { OpenSharePointDocument(item["{Identifier}"], item["{FilenameWithExtension}"]) };          
          spanText.innerText = "Download";
          spanIcon.classList.add("glyphicon-save-file");
        }
        btn.appendChild(spanIcon);
        btn.appendChild(spanText);       
        td_open.appendChild(btn);
        tr.appendChild(td_open);
        table.appendChild(tr);
      });
      container.appendChild(table)
    } else {
      let spanMessage = document.createElement("span")
      spanMessage.innerText = "No files found."
      container.appendChild(spanMessage);
    }
  }
</script>

Summary of the code:

  • There is basic HTML for the page + css styles for the spinner used to display progress
  • The script tag contains several functions:
    • On the DOMContentLoaded event, we add a listener to call the function LoadSharePointFolderContents without parameters
    • The LoadSharePointFolderContents function loads folder files list from SharePoint by calling the flow “Power Pages – List SharePoint folder contents“. If we call this function passing a parameter (folder path), it will be sending it to the cloud flow and listing subfolder contents.
      • If the Flow call in the LoadSharePointFolderContents function is successful, it will then call the GenerateSharePointTable function to render the table with contents, and also the GenerateBreadcrumb function to render the navigation path above the table.
    • In the GenerateSharePointTable function, we parse the contents received from the flow and then generate rows with action buttons depending on the item being a file or a folder.
      • If the item is a file, the button calls the function OpenSharePointDocument and that calls the flow “Power Pages – Get SharePoint File Content as Base64” passing the file id (path) to retrieve the file contents as base64, then it calls the DownloadBase64File function to download the file.
      • If the item is a folder, the button will call the function LoadSharePointFolderContents to refresh the page contents with the files from that folder, and update the breadcrumb accordingly
    • The GenerateBreadcrumb function generates the clickable breadcrumb so the user can navigate back and forth into the SharePoint document library folders.

Note: edit the flow URL variables on the beginning of the script tag to be the corresponding URLs you copied in the previous steps:

_getFileAsBase64FlowUrl: Url for the flow “Power Pages – Get SharePoint File Content as Base64

_getFolderContentFlowUrl: Url for the flow “Power Pages – List SharePoint folder contents

This code sample is using the embedded script in the page to be simple, but you can also split it in a separate web file for better organisation.

Save the page, preview the site and navigate to the new page.

Results

In this new page, you can browse the SharePoint document library, and download documents that are stored there:

Conclusion

By using the combination of JavaScript and Power Automate, you can enable integration scenarios that are not available natively in Power Pages.

Known issues

This approach works fine for files under 100MB only. Due to known Power Automate limitations, when the contents of the ‘Get file contents’ action go over 104857600 bytes, you will get the following error:

The total payload size including both file and data, must not exceed 100 MB for it to work.

References

Configure Cloud Flows integration in Power Pages – Microsoft Learn

Limits of automated, scheduled and instant flows – Message Size – Microsoft Learn

Create and assign Web Roles – Microsoft Learn

3 comments

Leave a Reply

Your email address will not be published. Required fields are marked *