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:
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:
Create the Power Automate flow to retrieve file contents
Create another flow as above, with the same trigger but following setup:
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:
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
The post Using JavaScript and Cloud Flows to download files from a SharePoint document library in Power Pages appeared first on michelcarlo.