Update
Please note that this has been superseded by a newer, better approach since the addition of the new File and Image columns in Dataverse. Here are my blog posts on leveraging these with the Portal Web API:
- How to retrieve Image columns using the Portal Web API
- How to retrieve File columns using the Portal Web API
- How to upload Image columns using Portal Web API
Note – At the time of writing this, Microsoft are still working on uploading to File columns using the Portal Web API
Here’s a preview of the end result, combined with some visual feedback (see my previous post on how to provide visual feedback)
In the previous post (Uploading profile photos to the Contact entityimage using the Portal Web API), I mentioned a shortcoming of the new Image data type in Dataverse as far as portals are concerned… The images can’t currently be included in liquid or portal forms.
To work around this, I use an approach of uploading images to a Note with a specific name. For the Tracks in my Spotify clone example, I attach the cover art image to a Note called Artwork. I can then reliably retrieve that using Fetch XML (see example at the end of this post :))
There are 4 main building blocks involved:
- A form allowing the user to select a single image
- jQuery to get the contents of the selected image, as well as some metadata
- Authenticate with the Portal Web API
- Use the Portal Web API to create a note regarding an existing track and upload the image as an attachment
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> <input id="artworkDocumentbody" accept="image/jpg,image/jpeg,image/gif,image/tif,image/tiff,image/png" type="file"> <a class="btn btn-default" id="submitArtwork" href="#">Upload</a> </form>
Step 2 – Add jQuery to get the contents of the selected file (in base64 format)
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. Also, we’ll get the filename and the mime type as we’ll need to set these attributes when we create the note
// When the submitArtwork link is clicked $( '#submitArtwork' ).click(function(e) { // Run the getImageFileContents function getFileContentsAndMetadata(); // Don't perform the default behaviour for the link i.e. navigation e.preventDefault(); });
Step 3 – Authenticate with the Portal Web API
Thanks to following Nick Doelman’s advice, this is a matter of a single line of liquid, including the contents of a Web Template I created earlier, called Portal Web API Wrapper:
{% 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 upload the image as an attachment regarding an existing record
In my example, I’m uploading the artwork to a Track record as part of a multistep form but this could be used in many different scenarios too. The main difference would be where you get the GUID of the record you’re attaching the file to. In many use cases, this would be a matter of getting the GUID passed in the URL, by using {{ request.params.id }}
function createArtworkNoteAPI(artworkDocumentbodyContents,artworkFileName,artworkMimeType) { var trackGUIDfromWebFormStep = $('input#EntityFormView_EntityID').val(); var dataObject={ "notetext": "*WEB*", "subject": "Artwork", "filename": artworkFileName, "mimetype": artworkMimeType, "documentbody": artworkDocumentbodyContents, "objecttypecode": "musdyn_track", "objectid_musdyn_track@odata.bind": "/musdyn_tracks(" + trackGUIDfromWebFormStep + ")" } console.log(JSON.stringify(dataObject)); webapi.safeAjax({ type: "POST", url: "/_api/annotations", contentType:"application/json", data: JSON.stringify(dataObject), success: function(res, status, xhr) { $('a#submitArtwork').html('<span class="glyphicon glyphicon-ok"></span>'); var entityID = xhr.getResponseHeader("entityid"); console.log("entityID: " + xhr.getResponseHeader("entityid")); } }); }
Sample web template for uploading images as note attachments using the Portal Web API
{% include 'Portal Web API Wrapper' %} <h1>Upload Artwork</h1> <form> <input id="artworkDocumentbody" accept="image/jpg,image/jpeg,image/gif,image/tif,image/tiff,image/png" type="file"> <a class="btn btn-default" id="submitArtwork" href="#">Upload</a> </form> <script> // When the submitArtwork links is clicked $( '#submitArtwork' ).click(function(e) { // Run the getImageFileContents function getFileContentsAndMetadata(); // Don't perform the default behaviour for the link i.e. navigation e.preventDefault(); }); function getFileContentsAndMetadata() { // Get the name of the selected file var artworkFileName = $('#artworkDocumentbody')[0].files[0].name; // Get the mime type of the selected file var artworkMimeType = $('#artworkDocumentbody')[0].files[0].type; // Get the content of the selected file var file = document.getElementById('artworkDocumentbody').files[0]; // If the user has selected a file if (file) { // Read the file var reader = new FileReader(); reader.readAsDataURL(file); // The browser has finished reading the file, we can now do something with it... reader.onload = function(e) { // The browser has finished reading the file, we can now do something with it... var artworkDocumentbodyContents = e.target.result; // Split the string at the first comma (removing the base64 prefix) artworkDocumentbodyContents = artworkDocumentbodyContents.substring(artworkDocumentbodyContents.indexOf(',')+1); // Run the function to upload to the Portal Web API, passing the file's contents, name and mime type as an inputs createArtworkNoteAPI(artworkDocumentbodyContents,artworkFileName,artworkMimeType); }; } } function createArtworkNoteAPI(artworkDocumentbodyContents,artworkFileName,artworkMimeType) { var trackGUIDfromWebFormStep = $('input#EntityFormView_EntityID').val(); var dataObject={ "notetext": "*WEB*", "subject": "Artwork", "filename": artworkFileName, "mimetype": artworkMimeType, "documentbody": artworkDocumentbodyContents, "objecttypecode": "musdyn_track", "objectid_musdyn_track@odata.bind": "/musdyn_tracks(" + trackGUIDfromWebFormStep + ")" } console.log(JSON.stringify(dataObject)); webapi.safeAjax({ type: "POST", url: "/_api/annotations", contentType:"application/json", data: JSON.stringify(dataObject), success: function(res, status, xhr) { $('a#submitArtwork').html('<span class="glyphicon glyphicon-ok"></span>'); var entityID = xhr.getResponseHeader("entityid"); console.log("entityID: " + xhr.getResponseHeader("entityid")); } }); } </script> <h1>Upload Audio</h1> <form> <input id="audioDocumentbody" accept="audio/mpeg,audio/mpeg3,audio/x-mpeg-3" type="file"> <a class="btn btn-default" id="submitAudio" href="#">Upload</a> </form> <script> $( '#submitAudio' ).click(function(e) { createAudioNote(); e.preventDefault(); }); </script> <script> function createAudioNote() { var audioFileName = $('#audioDocumentbody')[0].files[0].name; var audioMimeType = $('#audioDocumentbody')[0].files[0].type; var file = document.getElementById('audioDocumentbody').files[0]; if (file) { // create reader var reader = new FileReader(); reader.readAsDataURL(file); reader.onload = function(e) { // browser completed reading file - display it //alert(e.target.result); var audioDocumentbodyContents = e.target.result; audioDocumentbodyContents = audioDocumentbodyContents.substring(audioDocumentbodyContents.indexOf(',')+1); createAudioNoteAPI(audioDocumentbodyContents,audioFileName,audioMimeType); }; } } function createAudioNoteAPI(audioDocumentbodyContents,audioFileName,audioMimeType) { var trackGUIDfromWebFormStep = $('input#EntityFormView_EntityID').val(); var dataObject={ "notetext": "*WEB*", "subject": "Audio", "filename": audioFileName, "mimetype": audioMimeType, "documentbody": audioDocumentbodyContents, "objecttypecode": "musdyn_track", "objectid_musdyn_track@odata.bind": "/musdyn_tracks(" + trackGUIDfromWebFormStep + ")" } console.log(JSON.stringify(dataObject)); webapi.safeAjax({ type: "POST", url: "/_api/annotations", contentType:"application/json", data: JSON.stringify(dataObject), success: function(res, status, xhr) { $('a#submitAudio').html('<span class="glyphicon glyphicon-ok"></span>'); var entityID = xhr.getResponseHeader("entityid"); console.log("entityID: " + xhr.getResponseHeader("entityid")); } }); } </script>
Bonus: Retrieve Notes using Liquid and Fetch XML and output attachments as HTML images
I guess it’s only fair to show you how to display the uploaded images in other web templates. For this, we’ll use liquid’s Fetch XML tag. This example is from my Spotify clone. It assumes that the GUID of an Album record is being passed in the URL. It retrieves the most recent note atatchment called ‘Artwork’ regarding this Album and outputs as an image. Please note that the mimetype and documentbody attributes aren’t available in Advanced Find, I had to add these manually:
{% assign id = request.params.id %} {% assign album = entities['musdyn_album'][request.params.id] %} {% fetchxml artwork_query %} <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false"> <entity name="annotation"> <attribute name="subject" /> <attribute name="notetext" /> <attribute name="filename" /> <attribute name="mimetype" /> <attribute name="documentbody" /> <attribute name="annotationid" /> <order attribute="subject" descending="false" /> <filter type="and"> <condition attribute="subject" operator="eq" value="Artwork" /> <condition attribute="isdocument" operator="eq" value="1" /> </filter> <link-entity name="musdyn_album" from="musdyn_albumid" to="objectid" link-type="inner" alias="aa"> <filter type="and"> <condition attribute="musdyn_albumid" operator="eq" uitype="musdyn_album" value="{{ id }}" /> </filter> </link-entity> </entity> </fetch> {% endfetchxml %} {% if artwork_query %} {% for result in artwork_query.results.entities %} <div class="cover-artwork"> <img class="img-responsive" src="/_entity/annotation/{{ result.annotationid }}" > </div> {% endfor %} {% endif %}
So over to you… how are you going to make use of the Portal Web API and multimedia uploads? I’d love to hear what amazing ideas are running through your head – leave a comment below 🙂