There is a special field type on ServiceNow forms called the “user_image”. This allows you to upload or delete an image right from the record form, as well as have it displayed. As we have been playing with custom interfaces and portals lately, we have been wanting a way to interact with these field types using AngularJS.
How do the user_image field types work
User_image field type still use the sys_attachment table as other attachments on a form. They just have a special convention surrounding them.
1) File name
The filename for the image has to be the name of the database field that is hosting the user_image. For example, on the “sys_user” table, there is a field called “photo”. If I were to upload an attachment to the record called “photo”, it would show up in the Photo field on the form.
2) Multiple files of same name
If there are multiple attachments on the form with that name, then you will just get one of them consistently on the form. You won’t see the others. If, for example, I had five files on the sys_user record, only one of them will show on the form. If I delete it from the form, then next one will show up automatically.
3) ZZ_YY Table Names
The sys_attachment table will list the image in a special format by default. When using the user_image interface on the form to upload an image, you will notice on the sys_attachment table that it saves the attachment but records the table with a prefix of “ZZ_YY”. This tells the system not to show the file as an attachment on the form. However, in the case of user_image files, it will show them in the field itself. If you were to remove the “ZZ_YY” prefix on the sys_attachment record, it would still show up on the field, but it would also show up in the attachment list at the top of the record.
Also, files without the ZZ_YY prefix take precedence over those with the ZZ_YY prefix on the user_image field.
Please note: as of the writing of this article (2016-April), the ServiceNow table API does not support the “ZZ_YY” prefix fully, so this solution will store user_image attachments in the regular format, causing the attachment to show up in the title of the form as well as being displayed in the field.
The snUserImage Angular Service
Because we have been using user_image fields quite a bit, I wanted to create a reusable AngularJS service that gives me some of my most used function around user_image fields. I created it as a service so that it could be loaded as part of any Angular app.
The snUserImage service contains the following functions which are documented in the code snippet I’ll show below.
- uploadImage
- getImageSysId
- deleteImage
- replaceImage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | var snResources = angular.module("snResources", []); snResources.service('snUserImage', ['$http', function ($http) { /** * Uploads a file to a specific User_Image field on a ServiceNow table * * @param {file} file The binary file object on the client device * @param {string} table_name The table where the User_Image field resides * @param {string} table_sys_id The sys_id of the record on the table for file image to be stored * @param {string} field_name The name of the field on the form (database name, not label) */ this.uploadImage = function(file, table_name, table_sys_id, field_name){ var urlUpload = "/api/now/v1/attachment/file?table_name="+table_name+ "&table_sys_id="+table_sys_id+ "&file_name="+field_name; $http.post(urlUpload, file, { transformRequest: angular.identity, headers: {'Content-Type': file.type, 'Accept': "application/json, text/plain, */*", 'X-UserToken': window.userToken} }) .then(function successCallback(response) { // this callback will be called asynchronously // when the response is available }, function errorCallback(response) { // called asynchronously if an error occurs // or server returns response with an error status. }); }; /** * Retrieves the current user_image's sys_attachment sys_id if there is one that exists * (Please note: as of 2016-Apr, the ServiceNow API we are using does not yet support ZZ_YY * table prefixes to hide the attachment from the record.) * * @param {string} table_name The table where the User_Image field resides * @param {string} table_sys_id The sys_id of the record we want to query * @param {string} field_name The name of the field on the form (database name, not label) * @param {function} return_val A callback function to execute once the record API returns its response */ this.getImageSysId = function(table_name, table_sys_id, field_name, return_val){ var urlGet = "/api/now/v1/attachment" + "?sysparm_query=table_name%3D"+table_name+ "%5Etable_sys_id%3D"+table_sys_id+ "%5Efile_name%3D"+field_name; var parent = this; $http.get(urlGet, { headers: {'Content-Type': "application/json", 'Accept': "application/json", 'X-UserToken': window.userToken} }) .then(function successCallback(response) { // this callback will be called asynchronously // when the response is available if( response && response.data && response.data.result && response.data.result.length && response.data.result.length > 0 ){ return_val(response.data.result[0].sys_id); } else { return_val(null); } }, function errorCallback(response) { // called asynchronously if an error occurs // or server returns response with an error status. }); }; /** * Deletes the specified attachment record (and hence deletes the image on the user_image field) * * @param {string} sys_id The sys_attachment record storing the User_Image file */ this.deleteImage = function(sys_id){ var urlDelete = "/api/now/v1/attachment/"+sys_id; $http.delete(urlDelete, { headers: {'Content-Type': "application/json", 'Accept': "application/json", 'X-UserToken': window.userToken} }) .then(function successCallback(response) { // this callback will be called asynchronously // when the response is available }, function errorCallback(response) { // called asynchronously if an error occurs // or server returns response with an error status. }); }; /** * Replaces the current user_image with a new image * (Please Note: as of 2016-Apr the standard ServiceNow REST API does not support * the ZZ_YY table prefix that allows you to see attachments hidden from the form. * However, it will still replace the image on the form.) * * @param {file} file The binary file object on the client device * @param {string} table_name The table where the User_Image field resides * @param {string} table_sys_id The sys_id of the record on the table for file image to be stored * @param {string} field_name The name of the field on the form (database name, not label) */ this.replaceImage = function( file, table_name, table_sys_id, field_name ){ var parent = this; this.getImageSysId( table_name, table_sys_id, field_name, function(sys_id){ if( sys_id ){ parent.deleteImage(sys_id); } parent.uploadImage(file, table_name, table_sys_id, field_name); }); }; }]); |
As of April, 2016, the Table and Attachment REST API‘s in service now do not support the replacing of an attachment. Therefore, if I want to replace an image, I have to check to see if an image exists, grab the sys_id of the image, and delete the image, all before I upload the new image. The “replaceImage” takes care of this for me.
Example
Here is a simple UI page and Angular App I created to demonstrate the usage of this new service.
1) I stored my snUserImage service in a UI Script on the ServiceNow Instance
2) I created a small Angular app / directive / controller in another UI Script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // Setting up my app and I am injecting my snResources service var myApp = angular.module('myApp', ['snResources']); // Using this to determine when I should hid the file Upload control on the page myApp.loaded = false; // Handy fileModel directive that allows me to capture the file information myApp.directive('fileModel', ['$parse', function ($parse) { return { restrict: 'A', link: function(scope, element, attrs) { var model = $parse(attrs.fileModel); var modelSetter = model.assign; element.bind('change', function(){ scope.$apply(function(){ modelSetter(scope, element[0].files[0]); }); }); } }; }]); // File upload controller myApp.controller('myCtrl', ['$scope', 'snUserImage', function($scope, snUserImage){ $scope.setImage = function(){ var file = $scope.myFile; snUserImage.replaceImage(file, "sys_user", window.currentUser, "photo"); $scope.loaded = true; }; }]); |
3) I created a UI page importing the angular code and building out a simple file input form
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | <j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null"> <!-- Load Angular using CDN for this example --> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script> <!-- Load my Angular App and my Services in UI Page using method found: https://john-james-andersen.com/blog/service-now/linking-several-javascript-libraries-servicenow-ui-pages.html This method helps to avoid unwanted browser caching issues --> <g:evaluate object="true"> var uiscripts = [ //BEGIN EDIT "jjaTestImageAppV1", //My angular app and controller "snResources" //The angular Resource for ServiceNow //END EDIT ]; var jslibs = []; for( var key in uiscripts ){ var gr = new GlideRecord('sys_ui_script'); gr.addQuery("name", uiscripts[key]); gr.query(); gr.next(); jslibs.push( {"name": gr.name, "ts": gr.sys_updated_on } ); } </g:evaluate> <j:forEach var="jvar_js_lib" indexVar="jvar_js_index" items="${jslibs}"> <g:evaluate jelly="true"> var js_lib_name = jelly.jvar_js_lib.name; var js_lib_ts = jelly.jvar_js_lib.ts; </g:evaluate> <g:requires name="${js_lib_name}.jsdbx" params="cache=${js_lib_ts}" /> </j:forEach> <!-- Get the User session and Current User Sys_id --> <g:evaluate object="true"> var token = gs.getSessionToken(); var user = gs.getUserID(); </g:evaluate> <script> window.userToken = "${token}"; window.currentUser = "${user}"; </script> <!-- Set up the Angular File control --> <div ng-app="myApp"> <div ng-controller = "myCtrl"> <div ng-show="!loaded"> <input type="file" file-model="myFile"/> <button ng-click="setImage()">upload me</button> </div> <div ng-show="loaded"> Success -- <a href='/sys_user.do?sys_id=${user}'>View the record</a> </div> </div> </div> </j:jelly> |
You can see an animation of the result here:
Very good John, like it a lot. Thanks for sharing this, it will definitely come in handy one day soon I think.