Introduction
Here are the main points I learned along my twisty-turny R&D journey trying to upload images using the Portal Web API:
- The Portal Web API’s part in the process is much easier than I imagined
- The way browsers handle the selected image is a bit weird and clunky (at least to me!) – that’s because at the time we need to interact with the image for use with the API, the file itself is still on the user’s local machine and we need a way to access it through the browser. I took the hit on this and hopefully saved you much banging your head against the wall 🙂
- For now, we need to dodge custom fields using Dataverse’s new(ish) Image data type if we wish to use the uploaded images as part of our portal content – in my case for album covers, track artwork, etc. because those images aren’t available to liquid and Power Pages
In this first example, we’ll add a simple form to a Web Template that allows users to upload a profile photo. Using the Portal Web API, we’ll save this to the entityimage field on the portal user’s contact record.
For the impatient readers (full disclosure, that’s usually me), you can jump straight to my sample code for uploading profile photos
Here’s how I arrived at that code
There are 4 pieces to this puzzle, most of which require very little code:
- A form allowing the user to select a single image
- jQuery to get the contents of the selected image in base64 format
(I say with confidence but have no clue what that actually means… it just works so no arguing please) - Authenticate with the Portal Web API
(I followed Nick Doelman’s sound advice and stored the code for this in a separate web template for easy reuse. See his excellent tutorial here: Test Driving Power Pages’ Portal Web API – ReadyXRM) - Use the Portal Web API to update the logged in portal user’s contact record, saving the selected image to the entityimage field
Step 1 – Add a form allowing the user to select a single image
The form contains a single input that accepts one image file, and an anchor tag (link) which we’ll use to trigger the upload functionality
<form id="profileUploadForm"> <input id="profilePhoto" accept="image/jpg,image/jpeg,image/gif,image/tif,image/tiff,image/png" type="file"> <a class="btn btn-default" id="submitPhoto" href="#">Upload</a> </form>
Step 2 – Add jQuery to get the contents of the selected file (in base64 format)
This is where I struggled the first time around. Here’s what I hadn’t understood…. at this stage, although there is a file selected, the contents of the image aren’t yet available in the browser and nothing has been uploaded. We need to tell jQuery to read the file and get its content in base64 format before we can send anything with the API
I’ve commented this throughout to try and help make sense of what each piece does…
// When the submitPhoto links is clicked $( '#submitPhoto' ).click(function(e) { // Run the getImageFileContents function getImageFileContents(); // And don't perform the default behaviour for the link e.g. navigation e.preventDefault(); }); function getImageFileContents() { // Get the first (and only) file selected in the profilePhoto field var file = document.getElementById('profilePhoto').files[0]; // If the user has selected a file if (file) { // Read the file var reader = new FileReader(); reader.readAsDataURL(file); reader.onload = function(e) { // The browser has now completed reading the file, do something with it... var profilePhotoContents = e.target.result; // Split the string at the first comma (removing the base64 prefix) profilePhotoContents = profilePhotoContents.substring(profilePhotoContents.indexOf(',')+1); // Run the function to upload to the Portal Web API, passing the file's contents as an input uploadPhotoAPI(profilePhotoContents); }; } }
Step 3 – Authenticate with the Portal Web API
Thanks to following Nick’s advice, this is a matter of a single line of liquid, including the contents of the Portal Web API Wrapper web template:
{% include 'Portal Web API Wrapper' %}
If you haven’t already saved the wrapper function for easy reuse each time you need to authenticate the logged in portal user with the Portal Web API, here are the steps:
- Open a new browser tab and create a Web Template called Portal Web API Wrapper
- Add opening and closing <script></script> tags
- Copy the Wrapper AJAX Function from the Microsoft Docs page: https://learn.microsoft.com/en-us/power-pages/configure/write-update-delete-operations#wrapper-ajax-function and paste inside your script tags
- Save the web template
- Add the following liquid:
{% include 'Portal Web API Wrapper' %}
Step 4 – Use the Portal Web API to update the logged in portal user’s contact record, saving the selected image to the entityimage field
function uploadPhotoAPI(profilePhotoContents) { webapi.safeAjax({ type: "PATCH", url: "/_api/contacts({{ user.id }})", contentType:"application/json", data: JSON.stringify({"entityimage": profilePhotoContents}), success: function(res, status, xhr) { $('#submitArtwork').html('&lt;span class="glyphicon glyphicon-ok"&gt;&lt;/span&gt;'); var entityID = xhr.getResponseHeader("entityid"); console.log("entityID: " + xhr.getResponseHeader("entityid")); } }); }
Sample web template for uploading profile photos using the Portal Web API
This code contains a few extras here compared to the snippets above:
- The fetch XML required to retrieve and display the profile photo (entityimage) field for the logged in portal user
- jQuery to replace the original profile photo (if the contact had previously uploaded one) with the uploaded image
<div id="showOnSuccess"> </div> <div id="hideOnSuccess"> {% fetchxml entity_image_query %} <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false"> <entity name="contact"> <attribute name="fullname" /> <attribute name="telephone1" /> <attribute name="contactid" /> <attribute name="entityimage" /> <order attribute="fullname" descending="false" /> <filter type="and"> <condition attribute="contactid" operator="eq" uitype="contact" value="{{ user.id }}" /> </filter> </entity> </fetch> {% endfetchxml %} {% if entity_image_query.results.entities.size > 0 %} {% for result in entity_image_query.results.entities %} <img class="img-responsive" data-entityimage="{{ result.entityimage | join: ',' }}" alt="Profile photo of {{ user.fullname }}" /> {% endfor %} {% endif %} <h3>Profile Photo</h3> <form id="profileUploadForm"> <input id="profilePhoto" accept="image/jpg,image/jpeg,image/gif,image/tif,image/tiff,image/png" type="file"> <a class="btn btn-default" id="submitPhoto" href="#">Upload</a> </form> </div> {% include 'Portal Web API Wrapper' %} <script> // When the submitPhoto links is clicked $( '#submitPhoto' ).click(function(e) { // Run the getImageFileContents function getImageFileContents(); // And don't perform the default behaviour for the link e.g. navigation e.preventDefault(); }); function getImageFileContents() { // Get the first (and only) file selected in the profilePhoto field var file = document.getElementById('profilePhoto').files[0]; // If the user has selected a file if (file) { // Read the file var reader = new FileReader(); reader.readAsDataURL(file); reader.onload = function(e) { // The browser has now completed reading the file, do something with it... var profilePhotoContents = e.target.result; // Split the string at the first comma (removing the base64 prefix) profilePhotoContents = profilePhotoContents.substring(profilePhotoContents.indexOf(',')+1); // Run the function to upload to the Portal Web API, passing the file's contents as an input uploadPhotoAPI(profilePhotoContents); }; } } function uploadPhotoAPI(profilePhotoContents) { $('#submitPhoto').html('Uploading...'); webapi.safeAjax({ type: "PATCH", url: "/_api/contacts({{ user.id }})", contentType:"application/json", data: JSON.stringify({"entityimage": profilePhotoContents}), success: function(res, status, xhr) { $('div#showOnSuccess').append('<img class="img-responsive" src="data:image/jpeg;base64,' + profilePhotoContents + '" />'); $('div#hideOnSuccess').hide(); var entityID = xhr.getResponseHeader("entityid"); console.log("entityID: " + xhr.getResponseHeader("entityid")); } }); } // Convert the entity image to a format that can be displayed in the portal function toBase64(str) { if (!str) return null; var uarr = new Uint8Array(str.split(',').map(function (x) { return parseInt(x); })); return btoa(String.fromCharCode.apply(null, uarr)); } // Find any entity images and convert the byte string to base64 window.addEventListener('load', function () { document.querySelectorAll('img[data-entityimage]').forEach(function (img) { var data = img.dataset && img.dataset.entityimage; var base64data = data ? toBase64(data) : null; if (base64data) { img.src = 'data:image/jpeg;base64,' + base64data; } }); }); //Credit for the code to display the entity image: https://saddamk.blogspot.com/2018/10/display-image-of-product-in-dynamics.html </script>
In the next post, we’ll see how to take images uploaded through the web API and attach them to existing records using the Note entity
Credit where it’s due… I’d never have worked out how to turn the entity image into something compatible with the portal. Here’s where I got that method: https://saddamk.blogspot.com/2018/10/display-image-of-product-in-dynamics.html
Could you please add screenshots of *where* the code is pasted into?
Hi Ryan. The code needs to go into a Web Template. In my case, I’ve created a custom web template for the Profile page, replacing the out-of-the-box web template using the steps shown in this post: https://francomusso.com/customising-the-profile-page-on-power-apps-portals
I’d strongly suggest that anyone new to Power Apps Portals master the basics before attempting anything with the Portal Web API. A good understanding liquid and some understanding of jQuery will give you a solid foundation.
How to upload file?
The easiest way now that Dataverse File columns are supported in forms (basic and multistep, in edit mode only) is simply to add a File column to your table and include that on the form.