Posts

Showing posts from May, 2019

UI Action Trigger New Approval

this can be used to modify the approver and re-send a new email restricted to sys admins (or whatever role desired) name: Trigger New Approval condition: current.state=='requested' && gs.hasRole("admin") script: function trigger_new_approval(){ var res=confirm('Have you updated the approver and are happy to trigger a new approval notification?'); if (res){ alert('re-sending approval to ' + g_form.getValue('approver')); gsftSubmit(null, g_form.getFormElement(), 'resend_email'); //MUST call the 'Action name' set in this UI Action } } if (typeof window == 'undefined'){ resend_approval(); } function resend_approval(){ var grAppr=new GlideRecord('sysapproval_approver'); var grUser=new GlideRecord('sys_user'); if (grUser.get('sys_id', current.approver)){ if (!gs.nil(grUser.email)){ gs.eventQueue('approval.inserted', current, grUser.sys_id, gr

UI Action Test Load 2 records

instead of the ootb test load records which requires certain privileged roles to access, create a new one which can be restricted to the itil role makes a httprequest call to the out of the box java processor (which itself cannot be edited) UI Action: "Test Load 2 Records (ServiceDesk)" table: data source (sys_data_source) condition: current.import_set_table_name=='u_<whatever table name>' && gs.hasRole('<whatever role>service_desk'); code: -------- -------- current.update(); try{ //--run the processor this way, so that service desk do not need import_transformer role to do this var url=gs.getProperty('glide.servlet.uri')+'sys_import.do'; var request=new GlideHTTPRequest(url); request.setBasicAuth(gs.getProperty('sd.uid'), gs.getProperty('sd.pwd')); request.addHeader('Accept', 'application/json'); request.addParameter('import_source', 'data_source

Locate trigger action from workflow

ui action table: wf_executing name: "Locate Matching sys_trigger entry" //--server side option: var url='/sys_trigger_list.do?sysparm_query=nameLIKE' + current.sys_id + '&sysparm_list_mode=grid'; action.setRedirectURL ( url ) ; > go to it and update the date to a few seconds from now ------------- how to enable the worklflow context ui action on the ticket: > do an insert and stay on the workflow context ui action, adding in the table it should display on (for example, hr case) script can be modified to: var url= '/wf_context_list.do?sysparm_query=id%3D'+ current.sys_id; action.setRedirectURL(url); clicking this should take you to the wf_executing entries (under the related tab), click into the wait block entry and you should see the new Ui action

kill old workflows

var sQuery="active=true^started<javascript:gs.beginningOfLast12Months()"; var grWF=new GlideRecord('wf_context'); grWF.addQuery(sQuery); grWF.query(); gs.print(grWF.getRowCount()); while (grWF.next()){   var grTask=new GlideRecord('task');   grTask.addInactiveQuery();   grTask.addQuery('sys_id', grWF.id);   grTask.query();   if (grTask.next()){      gs.print('ticket ' +  grTask.number);      grWF.state='cancelled';      grWF.active=false;      var cancel_sysid=grWF.update();      gs.print(cancel_sysid + ': cancelled workflow context');      //break;   } }

Attach a file to email for users with no roles

attach a file to a published KB article then link to the attachment from an email notification script: (function runMailScript(/* GlideRecord */ current, /* TemplatePrinter */ template, /* Optional EmailOutbound */ email, /* Optional GlideRecord */ email_action, /* Optional GlideRecord */ event) { //--as per https://docs.servicenow.com/bundle/london-servicenow-platform/page/administer/notification/concept/c_AttachingDocsToANotification.html var url='/sys_attachment.do?sys_id=1d9ab0f31b5db300cee5ed7cee4bcb08&sysparm_viewer_table=kb_knowledge&sysparm_viewer_id=497a30f31b5db300cee5ed7cee4bcb81'; //--had to add the attachment to a KB article in order to be able to allow non-roled users to access the attachment template.print ('<a href="' + url + '">Click to download the 6th floor plan</a>'); })(current, template, email, email_action, event);

Get bits from the URL

get entire url: var instanceURL = GlideTransaction.get().getRequest().getHeader("referer"); get bit of a url: var surl='https://glddev.service-now.com/hr/case_detail.do?sysparm_case=210ebaf74f82ef00aaa42c518110c7a9&sysparm_newcase=true&sysparm_nameofstack=250efaf74f82ef00aaa42c518110c705'; //--bit between 'sysparm_newcase' and '&' var testRE = surl.match("sysparm_newcase=(.*)&"); gs.print(testRE);

variable advanced reference qualifier example

javascript: 'sys_idIN'+new hld_hr_functions().getHRRefQual('hr_openedfor_returner'); *************** script incl funcs: getHRRefQual: function(refQualID){ //--build up ref quals var sRefQualQuery=""; if (refQualID=='hr_openedfor_returner'){ //--called from here: /nav_to.do?uri=item_option_new.do?sys_id=271a5f781b766f00cee5ed7cee4bcb60 //sRefQualQuery="nameISNOTEMPTY^u_technical_user=false^" + this._DormantAccountUsers(); sRefQualQuery=this._DormantAccountUsers(); } return sRefQualQuery; }, _DormantAccountUsers: function(){ //--use below to query dormant accounts table var grDorm=new GlideRecord('u_dormant_accounts'); var sQuery='u_user.nameISNOTEMPTY^u_user.u_technical_user=false^u_user.last_login_time<javascript:gs.beginningOfLast3Months()^ORu_user.last_login_timeISEMPTY'; grDorm.addQuery(sQuery); //grDorm.addQuery('u_user.active=false'); grDorm.addNotNull

XML Parse example (on ecc queue table)

var grECCQ=new GlideRecord('ecc_queue'); //var sysid='8929f7ec4fed2700ddd0ecd18110c765'; sysid='2d616f684f2d2700ddd0ecd18110c71c'; if (grECCQ.get('sys_id', sysid)){    var pl_xml=new XMLDocument(grECCQ.payload); var inodes= pl_xml.getNodes("//parameter"); for (x=0;x<inodes.length;x++){ //gs.print(inodes.item(x).getAttribute('name') + '::: ' + inodes.item(x).getAttribute('value')); var sname=inodes.item(x).getAttribute('name'); //if (sname=='sequence'){ if (sname=='error_string'){    sValue=inodes.item(x).getAttribute('value');   gs.print('sValue=' + sValue);    if (sValue!=''){         gs.print('ERR!!');    } } } var sErrChk2=pl_xml.getNodeText("//error"); gs.print(sErrChk2); }

URL link in addInfoMessage

var ga=new GlideAjax('gld_HR_ajax'); ga.addParam('sysparm_name', 'checkEmployeeNumber_hrProfile'); ga.addParam('sysparm_hrprofilenumber', g_form.getValue('number')); ga.addParam('sysparm_employeenumber', newValue); ga.getXMLAnswer(function(answer) { if (answer!='undefined' && answer!=''){ var navURL="<a style='text-decoration:underline;color:blue' href=hr_profile.do?sysparm_query=number=" + answer + ">" + answer + "</a><img width='3' src='images/s.gif'/>"; var sMsg='The employee number entered already exists on another HR Profile ' + navURL; //alert(sMsg); g_form.showErrorBox('employee_number', 'error - please check'); g_form.addInfoMessage(sMsg); } });

UI Macro: attachment file for record producer

<?xml version="1.0" encoding="utf-8" ?>  <j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">  <a onclick="saveCatAttachment(gel('sysparm_item_guid').value, 'new_call')">  <img title="Attachments..." height="16" src="images/icons/attachment.gifx" border="0" width="16"/> Click here</a>  to attach items to this request. </j:jelly>

UI Macro: link to CIs assigned to user

<?xml version="1.0" encoding="utf-8" ?> <j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">   Assets currently assigned to this user: <a href='' onclick="event.preventDefault();gdprRef('${gs.getMessage('gdpr')}');">click here</a> <script>   function gdprRef(url){   var req_for=g_form.getValue('var_requested_for');   if (req_for!=''){ var dialog = new GlideDialogWindow('show_list'); dialog.setTitle('Assets assigned to user'); dialog.setPreference('table', 'cmdb_ci_list'); dialog.setPreference('sysparm_view', 'default'); var query = 'assigned_to=' + req_for; dialog.setPreference('sysparm_query', query);     dialog.render();   } }

transform script: get import set row sysid (oncomplete script) in transform map

gs.log('import set sysid test:' + import_set.sys_id , 'trMap:complete:LongTermUser');

deal with leading / trailing spaces

var ciclass='u_cmdb_ci_laptops'; //ciclass='u_smart_devices'; var querytorun='nameISNOTEMPTY^install_status!=14'; //var querytorun='nameISEMPTY'; var gr=new GlideRecord(ciclass); gr.addQuery(querytorun); gr.query(); while (gr.next()){ if (startsOrEndsWithWhitespace(gr.name)){ //if (startsOrEndsWithWhitespace(gr.sys_id)){//--for empty name query gs.print(gr.name); //gs.print(gr.sys_id); //--for empty name query } } function startsOrEndsWithWhitespace(str){ return /^\s|\s$/.test(str); }

Skip workflows, business rules and last updated value

gr.autoSysFields(false);//--leave last updated intact gr.setWorkflow(false); //--skip business rules and notifications gr. setEngines ( false ) ; //--skip data policy rules

check if time out of hours

--business hours in this example 8am-5pm: var gdt=gs.nowDateTime(); var gdtArr= gdt.split(' '); var timeArr=gdtArr[1].split(':'); //vat currenttime=parseInt(timeArr[0]); gs.print(timeArr[0]); if (timeArr[0]>=17 || timeArr[0]<8){    gs.print('out of hours'); }

set variables read only in a client script

g_form.setVariablesReadOnly(true);

search for a ticket based on variable value

var max_records=3; var sQuery='active=false^sys_created_onONLast 60 days@javascript:gs.beginningOfLast60Days()@javascript:gs.endOfLast60Days()'; var sShortDescrQuery='^short_descriptionLIKEchange'; var variableValueToCheck='true'; //--the more refined the query, the quicker it will run //--run it in a test environment first! //getMatchingTickets('hr_case', '', 'ict_required', '', 'Yes', sQuery, max_records); //getMatchingTickets('hr_case', '', '', 'ICT Required?', 'Yes', sQuery, max_records); //getMatchingTickets('hr_case', '', 'loan_secondment_return', '', 'true', sQuery, max_records); //getMatchingTickets('hr_case', '', 'maternity_career_return', '', 'true', sQuery, max_records); //getMatchingTickets('hr_case', '', 'loan_secondment_start', '', 'true', s

re-start a workflow

var gr = new GlideRecord('wf_context'); gr.addQuery("id", current.sys_id); gr.addInactiveQuery(); gr.addQuery("name","HR case placed on hold"); gr.query(); //while (gr.next()){ if (gr.next()){ /*gs.addInfoMessage(gr.active); if (gr.active && current.state!=previous.state){ //--cancel the old workflow gs.addInfoMessage('cancelling previous on hold workflow'); var wf = new Workflow(); wf.cancel(gr); }*/ gs.addInfoMessage('restarting "HR case placed on hold" workflow'); var wflw = new Workflow(); wflw.startFlow(wflw.getWorkflowFromName('HR case placed on hold'), current, 'update'); //break; }

remove the delete option from action on selected records (ui action on list)

go to the list view delete ui action and insert a copy for the table in question, set the condition field to 'false'

record producer variables in a business rule

//--new code, RDS Dec2018 g_scratchpad.rp_sysid=''; var producerVars_allVars = new GlideRecord('question_answer'); //var allVars = []; producerVars_allVars.addQuery('table_sys_id', current.sys_id); //--Exclude Label and Container variables producerVars_allVars.addQuery('question.type', '!=', 11); producerVars_allVars.addQuery('question.type', '!=', 19); producerVars_allVars.addQuery('question.type', '!=', 20); //--note: UI Macro is question.type=14 producerVars_allVars.query(); //gs.addInfoMessage(current.producer.sys_id); while(producerVars_allVars.next()){ //gs.addInfoMessage(producerVars_allVars.question.type + ' ' + producerVars_allVars.question.name.toString()  + ' ' + producerVars_allVars.value.toString()); if (producerVars_allVars.question.name.toString()=='hr_rp_sysid'){ g_scratchpad.rp_sysid=producerVars_allVars.value;

new record: prepopulate values via url

/new_call.do?sys_id=-1&sysparm_query=call_type=general_inquiry^short_description=General Enquiry

notify when MID switched on in non prod

via event. buiness rule (ecc_agent table) (function executeRule(current, previous /*null when async*/) { //--double check that the environment is preprod var instURL=gs.getProperty('glide.servlet.uri'); if (instURL.toLowerCase()=='https://<>preprod.service-now.com/'){ gs.eventQueue('preprod.mid.switchedon', current,'',''); //--now, queue the event on prod also so the email can actually fire try{ //--run the processor this way, so that service desk do not need import_transformer role to do this var url='https://<>prod.service-now.com/preprodmid.do'; var request=new GlideHTTPRequest(url); request.setBasicAuth(gs.getProperty('proc_uid'), gs.getProperty('proc_uid'));//--user in SN and credentials replicated to sys properties request.addHeader('Accept', 'application/json'); //request.addParameter('', ''); var response=request.get();

Set a record producer variable

var si=new hld_hr_functions(); //setRPVariable: function(table name, ticket sysid, variable label, variable name, new variable value){ si.setRPVariable('hr_change', 'f886c7881bf4b300cee5ed7cee4bcb15','','snc_located_in_ad', 'BLAH'); script include code: setRPVariable: function(table, ticketSYSID, optVariableName, optVariableName_db, new_value){ //--update a record producer variable value //--called from a HR workflow (HR Leaver Returning with Old Trent ID) //--reference: //--https://docs.servicenow.com/bundle/london-application-development/page/script/server-scripting/concept/c_ScriptableServiceCatalogVariables.html var gr= new GlideRecord(table); gr.addQuery('sys_id', ticketSYSID); gr.query(); var vars=[]; if (gr.next()) { for (var prop in gr.variables) { if (gr.variables.hasOwnProperty(prop) ){ var variable =  gr.variables[prop].getDisplayValue(); var v = gr.variables[prop];

How to skip a workflow timer (using admin only ui action)

https://community.servicenow.com/community?id=community_question&sys_id=232ec3eddb9cdbc01dcaf3231f9619fa go to sys_trigger look for name contains WFTimer try and locate by name contains sysid of the active timer record; failing that, search for the matching next action field on date/time stamp update the sys_trigger.next action to a few seconds from now watch the workflow execute --- UI ACTIONS ON wf_executing table: ---non-client ui action redirect: var url='/sys_trigger_list.do?sysparm_query=nameLIKE' + current.sys_id + '&sysparm_list_mode=grid'; action.setRedirectURL ( url ) ; UI Action - client script version: client=true onclick=go_to_trigger() function go_to_trigger(){ //alert('hello'+g_form.getUniqueValue()); var grTr=new GlideRecord('sys_trigger'); grTr.addQuery('nameLIKE' + g_form.getUniqueValue()); grTr.query(); if (grTr.next()){ //alert('in GR'); var url = new GlideURL(&#

how to identify which record producer generated a ticket

use this example, sys id changed to that of the ticket in question: /sc_item_produced_record_list.do?sysparm_query=task%3Dc077cf90db6267c0adacb1b239961909&sysparm_list_mode=grid

fix script to kill old ageing workflows

these workflows may be stuck in limbo on tickets since closed var sQuery="active=true^started<javascript:gs.dateGenerate('2018-01-02','00:00:00')"; var grWF=new GlideRecord('wf_context'); grWF.addQuery(sQuery); grWF.query(); gs.print(grWF.getRowCount()); while (grWF.next()){   var grTask=new GlideRecord('task');   grTask.addInactiveQuery();   grTask.addQuery('sys_id', grWF.id);   grTask.query();   if (grTask.next()){      gs.print('ticket ' +  grTask.number);      grWF.state='cancelled';      grWF.active=false;      var cancel_sysid=grWF.update();      gs.print(cancel_sysid);      //break;   } }

Enforce max number of rows displayed in lists

might be needed for performance reasons to do this: go to system properties > UI properties > edit the "Items per page' drop-down options (comma separated, no spaces):" box fix script to tidy up old user preferences: -------------------------------------------------- updatePreferences('name=rowcount^value>=20^value!=20'); updatePreferences('value=100'); function updatePreferences(sQuery){ var grUserPreferences=new GlideRecord('sys_user_preference'); grUserPreferences.addQuery(sQuery); grUserPreferences.query(); gs.print('TOTAL: ' + grUserPreferences.getRowCount()); //grUserPreferences.setValue('rowcount',  '20'); //grUserPreferences.updateMultiple(); while (grUserPreferences.next()){ grUserPreferences.value='20'; grUserPreferences.update(); gs.print(grUserPreferences.user.name + ' updated'); } }

Copy attachment to target ticket

if (!gs.nil(sysID)){ var sysAttachment = new GlideRecord('sys_attachment'); sysAttachment.addQuery('table_sys_id',current.sys_id); sysAttachment.query(); var bCopy=false; while(sysAttachment.next()){ gs.addInfoMessage("Copying attachment " + sysAttachment.file_name + " to target ticket " + gr.number +" [table:" +  ctype + "]"); bCopy=true; } if (bCopy){ GlideSysAttachment.copy('new_call',current.sys_id,ctype,gr.sys_id);//--copies them all in one go } }

processor: fire an email when a MID is switched on in sub production

Solution for when email sending is switched off in sub production but switching on a MID could target prod systems (e.g. LDAP), a web service call is made to invoke a prod processor to queue the notification from prod using a processor call on prod business rule - RUNS ON SUBPROD: send alert when MID server switched on in UAT - processor via glidehttpsrequest ---------- (function executeRule(current, previous /*null when async*/) { //--double check that the environment is preprod var instURL=gs.getProperty('glide.servlet.uri'); if (instURL.toLowerCase()=='https://preprod.service-now.com/'){ gs.eventQueue('.preprod.mid.switchedon', current,'',''); //--now, queue the event on prod also so the email can actually fire try{ //--run the processor this way, so that the notification can be sent from production (business rule runs in preprod where notifications are switched off) var url='https://<prod>.servic

widget: prod changes

  HTML ___________________________ <!--span option:--> <h1>ServiceNow Production Changes</h1> <h2>Listed in order of most recent</h2> <h3>{{c.numberOfTickets}} change requests</h3> <div style="border: 2px solid black">   <div ng-repeat="item in c.chgsList" ng-class="'color' + ($index % 2)">   <span><b><a href="change_request.do?sysparm_query=number={{item.number}}">{{item.number}}</a></b></span><span>{{item.short_description}}</span><span> (deployed on: {{item.start_date}})</span>    <span class="formattxt">{{item.description}}</span>   <br/><br/> </div> </div>     <!--table option:--><!-- <div class="chgwrap" >   <h1>ServiceNow Production Changes</h1>   <h2>Listed in order of most recent</h2>     <tabl

widget: get RITMs linked to HR case

RITMs generated from HR workflow via an order guide, where a common hr_case_ref variable is in use on the RITMs and populated via the HR workflow (passed from order guide to child RITM)   HTML ___________________________ <title>Check RITMs for the HR Case</title> <div class='container'> <!-- your widget template -->  <table width="100px" border="0">      <tr><td>      <br/>      <label>Enter HR Case number here:</label>      <input value='HRM0000126' id='hr_case'/>      <br/>    </td></tr>    <tr><td>      <label>Enter order guide name here (optional):</label>      <input id='order_guide' ng-model="inputOG">        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;      <button ng-click="blankOrderGuideField()" class="btn b

AngularJS: auto expand text area

HTML: -------- <textarea sn-resize-height="trim" class="form-control" id='results_vars' rows="10" cols="100"> {{c.variablesTitle}}{{c.variablesList}}</textarea>

generate trigger from business rule

function onAfter(current, previous) {   // Create trigger to set HR Case to Work in Progress   // when On Hold review date passes   try {     // Search for any pre-existing triggers     var trigger = new GlideRecord("sys_trigger");     trigger.addQuery("document_key", current.sys_id);     trigger.addQuery('name', 'STARTSWITH', 'HR Case OnHold Review - ');     trigger.query();     while (trigger.next()) {       // Remove trigger(s) for this document       trigger.deleteRecord();     }         // Set review date and set time to start of day     var requestDate = new GlideDateTime(current.u_review_date);     var datestr = requestDate.getLocalDate().toString();     if (requestDate.isDST()) {       requestDate.setInitialValue( datestr + " 09:00:00" );     } else {       requestDate.setInitialValue( datestr + " 08:00:00" );     }     gs.log('Set On Hold Review Date for ' + current.number + ' to

hide options label

//--warning - not service portal friendly! function hideLabels(){ //--hide the annoying 'options' labels! (may need to rework this later for Service Portal) //$('label_IO:d6664818dba36300e5d0d8e74b9619ad').hide(); var  labels = document.getElementsByTagName('span'); var x; for (x in labels) { if (labels[x].innerText=='Options'){ ////labels[x].style.display = "none"; labels[x].hide(); } } }

GlideAggregate: duplicates

var ga= new GlideAggregate('task'); ga.addAggregate('COUNT', 'number'); ga.addHaving('COUNT', '>', 1); ga.query();     var dupls=[];     dupls.push("The following " + ga.getRowCount() + " records are duplicate on number ");   while (ga.next()) {         var DuplLine=ga.getAggregate('COUNT', 'number') + " => " + ga.getElement('number');      dupls.push(DuplLine); gs.print(DuplLine);  }

Tags input directive for AngularJS

http://mbenford.github.io/ngTagsInput

Service Portal UI Bootstrap directives / references

https://getbootstrap.com/docs/3.4/css/ Bootstrap UI Directives: http://angular-ui.github.io/bootstrap Bootstrap Description Lists: http://getbootstrap.com/docs/3.3/css/#type-lists UI Bootstrap uib-tabset and uib-tab directives: http://angular-ui.github.io/bootstrap/#!#tabs

SN Dev Youtube channel

https://www.youtube.com/channel/UCdXorgCT87YlFRN9n8oJ7_A

SN Dev Youtube channel

https://www.youtube.com/channel/UCdXorgCT87YlFRN9n8oJ7_A

Service Portal $sp.getRecordDisplayValues

SERVER SCRIPT: data.list = [] var gr=new GlideRecord('incident'); .. while (gr.next()){    var incident={}    $sp.getRecordDisplayValues (incident, gr, 'name,state, ... ');    data.list.push (incident); } HTML: <th ng-model="c.searchTerm"> <tr ng-repeat="inc in c.data.list | filter: c.searchTerm"> <td>{{inc.number}} <td>{{inc.state}} <td> ...

Javascript console

ctrl+shift+J

widget with table summary (count)

Image
looks like this HTML <form>   <!-- Step 2. Display initial data from the server -->   <div class="panel panel-default">     <!-- Default panel contents -->     <div class="panel-heading">Table Summary: Registrations</div>     <!-- List group -->     <ul class="list-group">       <li class="list-group-item"           ng-repeat="s in c.data.summary"           ng-click="c.selectAg(s)">{{ s.agName }}         <span class="badge">{{ s.agCount }}</span>       </li>     </ul>   </div>   <!-- Step 5. Display customized content based on user input -->  <pre>{{c.data.records | json}}</pre> </form> <!-- copied from:  https://getbootstrap.com/docs/3.3/components/#panels-list-group   --> CSS .badge{background:red;} CLIENT   function() { /* widget controller */ var c =

fontawesome in widget html

example: <span class=" fa fa-times "></span>

k19 session slides

http://spr.ly/6046Eglio

Angular services

angular services you can inject to the controller  https://code.angularjs.org/1.5.11/docs/api/ng/service

debug widgets

Image
select ctrl-right click (windows machine) to bring up the debug dialog can also output the data object in html to debug {{  data | json }}

GlideRecord setValue

setValue(String name, Object value) Sets the specified field to the specified value. Normally a script would do a direct assignment, for example,  gr.category = value . However, if in a script the element name is a variable, then  gr.setValue(elementName, value)  can be used. When setting a value, ensure the data type of the field matches the data type of the value you enter. This method cannot be used on journal fields. If the value parameter is null, the record is not updated, and an error is not thrown https://developer.servicenow.com/app.do#!/api_doc?v=madrid&id=r_GlideRecord-setValue_String_Object

Bootstrap alerts

see https://getbootstrap.com/docs/3.3/components/#alerts

ng-required

makes form fields mandatory ng-required="true"

Service Portal AngularJS and Bootstrap references

https://getbootstrap.com/docs/3.4/css/ https://docs.angularjs.org/api https://angular-ui.github.io/ https://angular-ui.github.io/bootstrap/ ------- For those people who want to have more in-depth coverage of scoping and $scope - controllerAs; please consult the following URLs: Understanding AngularJS | Pluralsight AngularJS: MVC implementation | Pluralsight AngularJS - Controllers: The Comprehensive How To | Pluralsight | Pluralsight AngularJS Models: The Scoop on Scopes | Pluralsight | Pluralsight

great Bootstrap references for building forms

https://bootsnipp.com/forms https://formden.com/form-builder/

ServicePortal: useful fundamentals guide

https://developer.servicenow.com/app.do#!/training/article/app_store_learnv2_serviceportal_madrid_service_portal_introduction/app_store_learnv2_serviceportal_madrid_service_portal_introduction_objectives?v=madrid

service portal widgets: use pre to display data.events as a JSON object

Image
enter anywhere in the HTML template of the widget  <pre>{{c.data.events}}</pre> (server script populates the data.events)

Align code in the widget editor

press ctrl/cmd+a to select entire script, followed by shift+tab to format it

Knowledge 19 kicks off...

Image

SN resources - ServiceNow Innovation

https://sc.service-now.com/snds