Power Apps Portals

How to upload to Image columns for existing records using the Portal Web API

Note – In case you’re impatient like me, there’s a full web template at the end of this post that includes displaying existing images, uploading and displaying new images, and even visual feedback… and error handling. You’re welcome 😊

Previously, I mentioned a shortcoming of the Image data type in Dataverse as far as portals are concerned… The images couldn’t be output on portals at the time (due to not being in the scope of liquid, FetchXML or forms. To work around this, I used 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’d then retrieve that using Fetch XML. Clunky but it worked.

All that is now a thing of the past thanks to read operations in the Portal Web API 😎 I’ve already blogged about how to retrieve Dataverse Image columns using the Portal Web API. Now let’s see how we can upload to Image columns using the Portal Web API.

In this example, I’ll use a standalone web page to upload to an Image column on an existing Track record. This can quite easily be adapted for create rather than update and/or included part of a basic form or advanced form .

There are 4 main building blocks involved:

  1. A form allowing the user to select a single image
  2. jQuery to get the contents of the selected image, as well as some metadata
  3. Authenticate with the Portal Web API
  4. Use the Portal Web API to upload the data to an Image column. This is the easy part!… it’s extracting the contents of the file and doing something with the result that gets little complicated
  5. (Optional) Display the uploaded image. In this example, I do so by setting the source of an img tag with the uploaded image. In other scenarios, you may prefer to append new img tags to the body / a specific div instead

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="image-upload">
<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.

// When the submitArtwork link is clicked
$( '#submitArtwork' ).click(function(e) {
// Run the getImageFileContents function
getImageFileContents();
// Don't perform the default behaviour for the link i.e. navigation
e.preventDefault();
});

function getImageFileContents() {
// 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 as an input
uploadToImageColumn(artworkDocumentbodyContents);
};
}
}

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:

  1. Open a new browser tab and create a Web Template called Portal Web API Wrapper
  2. Add opening and closing <script></script> tags
  3. Copy the Wrapper AJAX Function from the Microsoft Docs page: https://docs.microsoft.com/en-us/powerapps/maker/portals/web-api-perform-operations#wrapper-ajax-function and paste inside your script tags
  4. Save the web template
  5. Add the following liquid:
  6. {% include 'Portal Web API Wrapper' %}

Step 4 – Use the Portal Web API to upload the data to an Image column on the existing record

In my example, I’m uploading the artwork to a Track record as part of a multi-step web 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 uploadToImageColumn(artworkDocumentbodyContents) {
// Provide visual feedback that the upload function has started (changing the button text)
$('#submitArtwork').html('Uploading...');

webapi.safeAjax({
type: "PUT",
url: "/_api/musdyn_tracks({{request.params.id}})/musdyn_artwork",
contentType:"application/json",
data: JSON.stringify({
"value":artworkDocumentbodyContents
}),
// On success
success: function() {
// Provide visual feedback that the upload function has started (changing the button text to a tick / check mark)
$('#submitArtwork').html('<span class="glyphicon glyphicon-ok"></span>');
// Show the new artwork by setting the source of the 'uploaded artwork' image
$('img#uploaded-artwork').attr('src','data:image/jpeg;base64,' + artworkDocumentbodyContents);
},
// On error
error: function(xhr, status, error){
// Change the text on the upload link back from 'Uploading...' to 'Upload'
$('#submitArtwork').html('Upload');
// Log the error detais to the console
var errorMessage = xhr.status + ': ' + xhr.statusText;
console.log(errorMessage);
}
});
}

Bonus: Retrieve the uploaded image

I guess it’s only fair to show you how to display the uploaded images in a web template. This example is from my Spotify clone. It assumes that the GUID of a Track record is being passed in the URL:

{% assign track_record = entities['musdyn_track'][request.params.id] %}
{% if track_record.musdyn_artworkid %}
<script>
// When the page loads, get the existing artwork for this Track record
$( document ).ready(function() {
webapi.safeAjax({
type: "GET",
url: "/_api/musdyn_tracks({{request.params.id}})/musdyn_artwork",
contentType:"application/json",
success: function(res) {
// If successful, set the source of the existing artwork image to the retrieved file
$('img#existing-artwork').attr('src','data:image/png;base64,' + res.value);
},
error: function(xhr, status, error){
// If error, log to the console
var errorMessage = xhr.status + ': ' + xhr.statusText;
console.log(errorMessage);
}
});
});
</script>
{% 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 πŸ™‚

Example Web Template – the finished article

<!-- Check a GUID has been provided in the URL -->
{% if request.params.id.size == 36 %}
    <!-- Retrieve the track record by GUID -->
    {% assign track_record = entities['musdyn_track'][request.params.id] %}
        {% if track_record %}
            <!-- Include the Portal Web API Wrapper web template to get an authentication token -->
            {% include 'Portal Web API Wrapper' %}
            <div class="container">
                <div class="col-md-6 col-sm-12">
                    <!-- This div is shown by default but will be hidden if the upload succeeds -->
                    <div class="hideOnSuccess">
                        <h1>Existing Artwork</h1>
                        <!-- If the artwork field is not populated, show the placeholder / not found image -->
                        {% unless track_record.musdyn_artworkid %}
                            <img id="existing-artwork" src="/artwork-not-found.svg" style="width:300px; height: auto;" />
                        {% else %}
                            <img id="existing-artwork" style="width:300px; height: auto;" />
                        {% endunless %}
                    </div>
                    <!-- This div is hidden by default but will be shown if the upload succeeds -->
                    <div class="showOnSuccess" style="display:none;">
                        <h1>Your New Artwork</h1>
                        <img id="uploaded-artwork" style="width:300px; height: auto;" />
                    </div>
                </div>
                <div class="col-md-6 col-sm-12">
                    <h1>Upload Artwork</h1>
                        <!-- A form to upload to images, retricted to accept only JPGs, GIF, TIFFs or PNGs -->
                        <form id="image-upload">
                            <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>
                        <!-- This bootstrap alert is hidden by default but will be shown if an error is encountered with the upload -->
                        <div class="alert alert-danger showOnError" role="alert" style="display:none;"><span class="glyphicon glyphicon-warning-sign" aria-hidden="true"></span> Upload failed. Please try again</div>
                </div>
            </div>

            <!-- If the track record has existing artwork -->
            {% if track_record.musdyn_artworkid %}
            <script>
                // When the page loads, get the existing artwork for this Track record
                $( document ).ready(function() {
                    webapi.safeAjax({
                        type: "GET",
                        url: "/_api/musdyn_tracks({{request.params.id}})/musdyn_artwork",
                        contentType:"application/json",
                        success: function(res) {
                            // If successful, set the source of the existing artwork image to the retrieved file
                            $('img#existing-artwork').attr('src','data:image/png;base64,' + res.value);
                        },
                        error: function(xhr, status, error){
                            // If error, log to the console
                            var errorMessage = xhr.status + ': ' + xhr.statusText;
                            console.log(errorMessage);
                        }
                    });
                });
            </script>
            {% endif %}

            <script>
            // When the submitArtwork link is clicked
            $( '#submitArtwork' ).click(function(e) {
                // Run the getImageFileContents function
                getImageFileContents();
                // Don't perform the default behaviour for the link i.e. navigation
                e.preventDefault();                
            });

            function getImageFileContents() {
                // 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 as an input
                        uploadToImageColumn(artworkDocumentbodyContents);
                    };
                }
            }
            function uploadToImageColumn(artworkDocumentbodyContents) {
                // Provide visual feedback that the upload function has started (changing the button text)
                $('#submitArtwork').html('Uploading...');

                webapi.safeAjax({
                    type: "PUT",
                    url: "/_api/musdyn_tracks({{request.params.id}})/musdyn_artwork",
                    contentType:"application/json",
                    data: JSON.stringify({
                        "value":artworkDocumentbodyContents
                    }),
                    // On success
                    success: function() {
                        // Provide visual feedback that the upload function has started (changing the button text to a tick / check mark)
                        $('#submitArtwork').html('<span class="glyphicon glyphicon-ok"></span>');
                        // Show the new artwork by setting the source of the 'uploaded artwork' image
                        $('img#uploaded-artwork').attr('src','data:image/jpeg;base64,' + artworkDocumentbodyContents);
                        // Hide any elements with the 'hideOnSuccess' class
                        $('div.hideOnSuccess').slideToggle();
                        // Show any elements with the 'showOnSuccess' class
                        $('div.showOnSuccess').slideToggle();
                        // Hide any elements with the 'showOnError' class
                        $('.showOnError').hide();
                    },
                    // On error
                    error: function(xhr, status, error){
                        // Change the text on the upload link back from 'Uploading...' to 'Upload'
                        $('#submitArtwork').html('Upload');
                        // Hide any elements with the 'hideOnError' class
                        $('.showOnError').show();
                        // Log the error detais to the console
                        var errorMessage = xhr.status + ': ' + xhr.statusText;
                        console.log(errorMessage);
                    }
                });
            }

            </script>
        {% endif %}
    {% else %}
    <!-- if the track record is not found, display a bootstrap alert -->
    <div class="container">
        <div class="alert alert-danger" role="alert">
            <span class="glyphicon glyphicon-warning-sign" aria-hidden="true"></span> Track record not found / no unique identifier provided in the URL. I don't know which track to upload artwork to
        </div>
    </div>
{% endif %}

Further considerations

Dataverse doesn’t optimise images for the web so it’s on us to do so – whether that be manually in something like Photoshop or Gimp, or automated using Power Automate and Encodian’s excellent connector.

Franco Musso

You may also like

Leave a reply

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