Power Pages

Create a Drag and Drop experience to upload Case Attachments

Introduction

Here’s the overall flow:

  • Add some placeholder HTML to act as our drop zone
  • Listen for drop events on the drop zone
  • Loop over each ‘dropped’ file, getting its contents and metadata
  • Upload each file as a note attachment regarding the case, using the portal web API
  • Generate a thumbnail of each uploaded file

For the impatient among you, here’s the final code for you to steal / test drive ๐Ÿ™‚

We’ll create all this functionality on a standalone page for now. In a following post, I’ll show you how to integrate this functionality into a multistep form

Stage 1 – Create a ‘drop zone’

Snippets

Boilerplate code for a ‘drop zone’

There isn’t a specific HTML element for a drop zone. Here we’ve got a div which we’ll make ‘drag & drop-able’ using JavaScript. Also, we’ll want somewhere to list / preview any files that have been uploaded – so I’ve included a list of files with a few placeholders to begin with

<!-- Div to act as a container for drag and rop events as well as any existing attachments -->
<div id="drop-zone">
    <p>Drag and drop files here</p>
    <!-- List to hold the preview thumbnails of any attached files -->
    <ul id="file-list">
        <li class="attachment-preview"><div data-id="put-note-guid-here" data-filename="Example filename.jpg"><img class="img-responsive" src="https://picsum.photos/300/300"></div></li>
        <li class="attachment-preview"><div data-id="put-note-guid-here" data-filename="Example filename.pdf"><div class="inner"><span class="glyphicon glyphicon-file" aria-hidden="true"></span> Example filename.pdf</div></div></li>
    </ul>
</div>

Drop zone styling

By default, the drop zone won’t look any different whether or not the user is interacting / dragging files. Here’s some example CSS to style the drop zone differently on dragover vs dragleave and drop. Note – it’s the JavaScript that will add / remove the classes that make these styles take effect

I’ve also included some styles for the file list…

#drop-zone {
    width: 100%;
    min-height: 200px;
    border: 5px dashed #7e878d;
    text-align: center;
    background-color: #c2cfd730;
    border-radius: 0.5em;
    padding: 1em;
    overflow-y: hidden;
  }
  
  #drop-zone.dragover {
    background: #c2cfd7;
    transition: background ease 0.3s;
  }
  
  .attachment-preview {
    width: 200px;
    height: 200px;
    float: left;
    border: 5px solid #adb6d2;
    margin: 0 1em 1em 0;
    border-radius: 0.5em;
    box-shadow: 0 2px 5px rgba(0,0,0,0.3);
    display: flex;
    justify-content: center;
    padding: 1em;
  }
  
  .inner {
      display: flex;
      align-items: center;
      overflow-wrap: anywhere;
      font-size: clamp(0.5em, 1em, 1em);
  }
  
  .attachment-preview .glyphicon {
      font-size: 3em;
      display: block;
      margin-bottom: 0.25em;
  }

Stage 2- The basics of drag & drop events

So far we have an area of the page styled to look like drag and drop but doesnโ€™t actually DO anything yet

Our mission for this stage isโ€ฆ

  • Respond to dragover, dragleave, drop
  • Provide some visual feedback by adding or removing a class (which we have styles for in the CSS)
  • Prevent that default behaviour of just opening the file in a new browser tab

Key skills covered

  • Listening for events with jQuery
  • Adding & removing classes to provide visual feedback (forcing different CSS rules to apply for dragover vs dragleave and drop)
  • Preventing the browser’s default behaviour for a specific event

Here are the key events we care about:

Event Description
dragover The user’s cursor is over the drop zone with one or more files selected
dragleave The user’s cursor has left the drop zone without dropping / releasing the files
drop One or more files have been released / ‘dropped’ in the drop zone – this our trigger for upload

Snippets

$(document).ready(function() {
    
    // Initialise a variable that references the "drop-zone" container defined in the HTML
    var dropZone = $('#drop-zone');

    // Add a dragover event listener to the dropZone
    dropZone.on('dragover', function() {
        // Add a class of 'dragover'. We'll use this to provide visual feedback
        $(this).addClass('dragover');
        return false;
    });

    // Add a dragleave event listener to the dropZone
    dropZone.on('dragleave', function() {
        // Remove the'dragover' class. We'll use this to provide visual feedback
        $(this).removeClass('dragover');
        return false;
    });

    // Add a drop event listener to the dropZone. This is our key event
    dropZone.on('drop', function(e) {
        // Prevent the default behavior of opening the file in the browser
        e.preventDefault();
        // Remove the'dragover' class. We'll use this to provide visual feedback
        $(this).removeClass('dragover');
    });
});

Useful links

Stage 3 – File content and metadata

In this stage we’ll get our hands dirty with reading files and accessing their contents. The aim is to get the base64 contents of the files plus the name and mimetype in preparation for uploading in the next stage

Key skills covered

  • Accessing files from the File Drag & Drop API
  • Looping over the files with jQuery
  • Using FileReader and triggering functionality onload of a file

Snippets

// Retrieve the dropped files from the event object using e.originalEvent.dataTransfer.files
var files = e.originalEvent.dataTransfer.files;

// Loop through the files array
$.each(files, function(index, file) {
    // Initialise a new file reader
    var reader = new FileReader();
    // What to do once the 
    reader.onload = function(event) {
        // Split the file contents after the first comma (removing the base64 prefix)
        var base64 = event.target.result.split(',')[1];
        var fileName = file.name;
        var fileType = file.type;
        var fileSize = file.size;
        // Run the uploadToNoteAttachment function, passing the base64 content, the file name and the file type
        uploadToNoteAttachment(base64,fileName,fileType);
    };
    // Read the file
    reader.readAsDataURL(file);
});

Useful links

Stage 4 – Upload dropped files using the Portal Web API

We’ll now take that file content and metadata and upload using the Portal Web API. Not confident writing API calls from scratch? No worries… Guido’s excellent Dataverse REST Builder tool has us covered ๐Ÿ˜Ž

Snippets

function uploadToNoteAttachment(base64,fileName,fileType) {
    var record = {};
    record.filename = fileName; // Text
    record.documentbody = base64; // Text
    record.subject = fileName; // Text
    // Set the Regarding field to a case with GUID taken from the id parameter in the URL
    record["objectid_incident@odata.bind"] = "/incidents({{ request.params.id }})"; // Lookup

    webapi.safeAjax({
        type: "POST",
        contentType: "application/json",
        url: "/_api/annotations",
        data: JSON.stringify(record),
        success: function (data, textStatus, xhr) {
            var newId = xhr.getResponseHeader("entityid");
            console.log(newId);
        },
        error: function (xhr, textStatus, errorThrown) {
            console.log(xhr);
        }
    });
}

Useful links

Stage 5 – Generate preview thumbnails

We’re already successfully uploading files as note attachments regarding the case – but how do our customers know? In this stage we’ll see generate thumbnail images / icons and populate our file list. We’re so close! ๐Ÿ‘

Key skills covered

  • The ‘special’ URL to retrieve note attachments: /_entity/annotation/putyourguidhere
  • JavaScript if statement to check the mime type and provide different output
  • Appending to the DOM using jQuery

Snippets

function generateThumbnail(guid,fileName,mimeType) {
    if(mimeType.startsWith('image')) {
        var thumbnail = '<li class="attachment-preview"><div data-id="' + guid + '" data-filename="' + fileName + '"><img class="img-responsive" src="/_entity/annotation/' + guid + '"></div></li>';
    } else {
        var thumbnail = '<li class="attachment-preview"><div data-id="' + guid + '" data-filename="' + fileName + '"><div class="inner"><span class="glyphicon glyphicon-file" aria-hidden="true"></span> ' + fileName + '</div></div></li>';
    }
    // Append the thumbnail to the file list
    $('ul#file-list').append(thumbnail);
}

Useful links

Stage 6 – Load existing attachments when the page first loads

So all good for freshly uploaded files and images. What if the user returns to this page or refreshes though? It’ll look like there’s nothing attached. Let’s now see how we can load any existing attachments using jQuery’s document ready event

Snippets

function getExistingAttachments() {
    webapi.safeAjax({
        type: "GET",
        url: "/_api/annotations?$select=annotationid,filename,mimetype&$filter=(_objectid_value eq {{ request.params.id }} and isdocument eq true)&$orderby=createdon desc",
        contentType: "application/json",
        success: function (data, textStatus, xhr) {
            var results = data;
            console.log(results);
            for (var i = 0; i < results.value.length; i++) {
                var result = results.value[i];
                // Columns
                var annotationid = result["annotationid"]; // Guid
                var filename = result["filename"]; // Text
                var mimetype = result["mimetype"]; // Text
                // Run the generateThumbnail function, passing the note GUID, file name and mimetype as inputs
                generateThumbnail(annotationid,filename,mimetype);
            }
        },
        error: function (xhr, textStatus, errorThrown) {
            console.log(xhr);
        }
    });
}

The complete solution

The final HTML & Liquid

{% include 'Portal Web API Wrapper' %}
<!-- Div to act as a container for drag and drop events as well as any existing attachments -->
<div id="drop-zone">
  <p>Drag and drop files here</p>
  <!-- List to hold the preview thumbnails of any attached files -->
  <ul id="file-list"></ul>
</div>

 

The final JS

 // When the web page / UI is ready
$(document).ready(function() {
    getExistingAttachments();
    
    // Initialise a variable that references the "drop-zone" container defined in the HTML
    var dropZone = $('#drop-zone');

    // Add a dragover event listener to the dropZone
    dropZone.on('dragover', function() {
        // Add a class of 'dragover'. We'll use this to provide visual feedback
        $(this).addClass('dragover');
        return false;
    });

    // Add a dragleave event listener to the dropZone
    dropZone.on('dragleave', function() {
        // Remove the'dragover' class. We'll use this to provide visual feedback
        $(this).removeClass('dragover');
        return false;
    });

    // Add a drop event listener to the dropZone. This is our key event
    dropZone.on('drop', function(e) {
    // Prevent the default behavior of opening the file in the browser
    e.preventDefault();
    // Remove the'dragover' class. We'll use this to provide visual feedback
    $(this).removeClass('dragover');

    // Retrieve the dropped files from the event object using e.originalEvent.dataTransfer.files
    var files = e.originalEvent.dataTransfer.files;

        // Loop through the files array
        $.each(files, function(index, file) {
            // Initialise a new file reader
            var reader = new FileReader();
            // What to do once the 
            reader.onload = function(event) {
                // Split the file contents after the first comma (removing the base64 prefix)
                var base64 = event.target.result.split(',')[1];
                var fileName = file.name;
                var fileType = file.type;
                var fileSize = file.size;
                // Run the uploadToNoteAttachment function, passing the base64 content, the file name and the file type
                uploadToNoteAttachment(base64,fileName,fileType);
            };
            // Read the file
            reader.readAsDataURL(file);
        });
    });
});

// Function to upload the note attachment using the Portal Web API
// Accepts the file content, file name and file type as parameters
function uploadToNoteAttachment(base64,fileName,fileType) {
    var record = {};
    record.filename = fileName; // Text
    record.documentbody = base64; // Text
    record.subject = fileName; // Text
    // Set the Regarding field to a case with GUID taken from the id parameter in the URL
    record["objectid_incident@odata.bind"] = "/incidents({{ request.params.id }})"; // Lookup

    webapi.safeAjax({
        type: "POST",
        contentType: "application/json",
        url: "/_api/annotations",
        data: JSON.stringify(record),
        success: function (data, textStatus, xhr) {
            var newId = xhr.getResponseHeader("entityid");
            console.log(newId);
            // On successful upload, run the generateThumbnail function, passing the GUID of the newly created note record, the file name and the file type
            generateThumbnail(newId,fileName,fileType);
        },
        error: function (xhr, textStatus, errorThrown) {
            console.log(xhr);
        }
    });
}

// Function to generate thumbnails with either an preview for images or a file icon for other file types
// Accepts a ntoe GUID, a file name and mime type as parameters
function generateThumbnail(guid,fileName,mimeType) {
    if(mimeType.startsWith('image')) {
        var thumbnail = '<li class="attachment-preview"><div data-id="' + guid + '" data-filename="' + fileName + '"><img class="img-responsive" src="/_entity/annotation/' + guid + '"></div></li>';
    } else {
        var thumbnail = '<li class="attachment-preview"><div data-id="' + guid + '" data-filename="' + fileName + '"><div class="inner"><span class="glyphicon glyphicon-file" aria-hidden="true"></span> ' + fileName + '</div></div></li>';
    }
    // Append the thumbnail to the file list
    $('ul#file-list').append(thumbnail);
}

// Function to retrieve any existing attachments regarding the current case (using the GUID from the URL)
function getExistingAttachments() {
    webapi.safeAjax({
        type: "GET",
        url: "/_api/annotations?$select=annotationid,filename,mimetype&$filter=(_objectid_value eq {{ request.params.id }} and isdocument eq true)&$orderby=createdon desc",
        contentType: "application/json",
        success: function (data, textStatus, xhr) {
            var results = data;
            console.log(results);
            for (var i = 0; i < results.value.length; i++) {
                var result = results.value[i];
                // Columns
                var annotationid = result["annotationid"]; // Guid
                var filename = result["filename"]; // Text
                var mimetype = result["mimetype"]; // Text
                // Run the generateThumbnail function, passing the note GUID, file name and mimetype as inputs
                generateThumbnail(annotationid,filename,mimetype);
            }
        },
        error: function (xhr, textStatus, errorThrown) {
            console.log(xhr);
        }
    });
}

The final CSS

#drop-zone {
    width: 100%;
    min-height: 200px;
    border: 5px dashed #7e878d;
    text-align: center;
    background-color: #c2cfd730;
    border-radius: 0.5em;
    padding: 1em;
    overflow-y: hidden;
  }
  
  #drop-zone.dragover {
    background: #c2cfd7;
    transition: background ease 0.3s;
  }
  
  .attachment-preview {
    width: 200px;
    height: 200px;
    float: left;
    border: 5px solid #adb6d2;
    margin: 0 1em 1em 0;
    border-radius: 0.5em;
    box-shadow: 0 2px 5px rgba(0,0,0,0.3);
    display: flex;
    justify-content: center;
    padding: 1em;
  }
  
  .inner {
      display: flex;
      align-items: center;
      overflow-wrap: anywhere;
      font-size: clamp(0.5em, 1em, 1em);
  }
  
  .attachment-preview .glyphicon {
      font-size: 3em;
      display: block;
      margin-bottom: 0.25em;
  }
Franco Musso

You may also like

2 Comments

  1. Would it be possible to add a delete button to each thumbnail to allow documents that have been uploaded in error to be deleted? Or some other straightforward way for users to trim the list of documents attached to the record?

    1. Great question. Yes definitely possible. Coincidentally enough, Iโ€™ve just implemented this for a client. Follow-up post coming soon ๐Ÿ™‚

Leave a reply

Your email address will not be published.

More in Power Pages