- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎03-24-2021 08:06 AM
Hello all,
We have a requirement to create a copy button for requests/requested items in the Service Portal to be used to copy closed requests so the user does not have to enter the same information into a new request. I have been researching what others have done, and have found:
1.It is suggested that there is already one OOB, however I do not find one in my personal dev instance when following the directions in How to make Copy button form expose on service portal?.
2. I have also found Button to copy and insert a new ticket into table, but it is specific to incidents and I am not sure how to adapt it to requests, which have variables that can vary widely.
3. I also took at look at the UI action on Requests in the platform, which I thought might serve as a template for creating a widget, but I am not sure if it would work:
var helper = new GlideappScriptHelper();
var request = helper.copyRequest(current);
gs.addInfoMessage(gs.getMessage('Request copied successfully'));
action.openGlideRecord(request);
Has anyone done this before, or can someone assist with how I might do this? Is it possible to adapt 2. or 3. above to my purpose or can someone point me in the right direction for finding the OOB copy button from 1. above?
Any help will be appreciated. Thanks!
Solved! Go to Solution.

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎03-24-2021 08:22 AM
Ok so yeah its like this widget to resubmit request on the ticket page, script include to build the URL, variable set called 'copy_variables_from_uri_ritm_id' with client script to parse the URL
Widget HTML:
<div ng-if="::c.data.show_button" class="panel panel-default b wrapper-md" ng-class="{'wrapper-md': options.native_mobile != 'true', 'wrapper-sm': options.native_mobile == 'true'}">
<button name="resubmit" ng-disabled="c.resubmitting" ng-click="c.redirectToResubmit()" class="btn btn-primary btn-block ng-binding ng-scope">
Resubmit Request
</button>
</div>
Widget Client script:
function() {
var c = this;
c.resubmitting = false;
c.redirectToResubmit = function() {
c.resubmitting = true;
c.server.get({
resubmitting: true
}).then(function(r) {
top.window.location = r.data.resubmit_url;
});
}
}
Widget Server script:
(function() {
var tableName = $sp.getParameter('table');
var recordId = $sp.getParameter('sys_id');
if(tableName && recordId) {
var recordGr = new GlideRecord(tableName);
recordGr.get(recordId);
if(!input) {
data.show_button = new CatalogVariableURLBuilder().hasVariableSet(recordGr, 'copy_variables_from_uri_ritm_id');
}
}
if(input && input.resubmitting) {
data.resubmit_url = new CatalogVariableURLBuilder().getResubmitURL('sc_cat_item', recordGr);
}
})();
CatalogVariableURLBuilder script include:
var CatalogVariableURLBuilder = Class.create();
CatalogVariableURLBuilder.prototype = {
initialize: function() {},
getJsonFromGr: function(ritmGr) {
var variablesObj = ritmGr.variables.getElements(false);
var outputObj = {};
for (var i = 0; i < variablesObj.length; ++i) {
var variable = variablesObj[i];
var question = variable.getQuestion();
if (question.getValue() && question.getName()) {
outputObj[question.getName()] = question.getValue();
}
}
return JSON.stringify(outputObj);
},
hasVariableSet: function(ritmGr, variableSetName) {
if(ritmGr.getTableName() != 'sc_req_item' || !ritmGr.getValue('cat_item')) {
return false;
}
var ioSetItem = new GlideRecord('io_set_item');
ioSetItem.addQuery('sc_cat_item', ritmGr.getValue('cat_item'));
ioSetItem.addQuery('variable_set.internal_name', variableSetName);
ioSetItem.query();
return ioSetItem.hasNext();
},
getResubmitURL: function(catItemPageId, ritmGr) {
var catItemSysId = ritmGr.cat_item.toString();
return "?id=" + catItemPageId + "&sys_id=" + catItemSysId + "&sysparm_copy_from_ritm_id=" + ritmGr.getUniqueValue();
},
type: 'CatalogVariableURLBuilder'
};
Variable set:
Onload client script (in this variable set)
function onLoad() {
var prop, propVal;
//get parameter from URL
var ritmId = getParameterValue('sysparm_copy_from_ritm_id');
console.debug('sysparm_variables URI parameter value: ' + ritmId);
if (!ritmId) {
//If parameter is empty, stop here.
console.debug('aborting due to a missing sysparm_copy_from_ritm_id URI parameter');
return;
}
var getVariableJson = new GlideAjax('CopyVariablesAjax');
getVariableJson.addParam('sysparm_name', 'getVariableJson');
getVariableJson.addParam('sysparm_copy_from_ritm_id', ritmId);
getVariableJson.getXMLAnswer(populateForm);
function populateForm(variableJsonStr) {
console.debug('sysparm_copy_from_ritm_id variables JSON object: ' + variableJsonStr);
var variableJson = JSON.parse(variableJsonStr);
for(var formVariable in variableJson) {
if(g_form.hasField(formVariable)) {
g_form.setValue(formVariable, variableJson[formVariable]);
}
}
}
}
function getParameterValue(name) {
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
//Check if portal (gel=undefined) or classic UI, and get the HREF data appropriately in either case.
var hrefPath = (typeof gel == 'undefined') ? this.location.href : top.location.href;
var results = regex.exec(hrefPath);
console.debug('HREF: ' + hrefPath);
if (results == null) {
return "";
} else {
return unescape(results[1]);
}
}
CopyVariablesAjax script include client callable:
var CopyVariablesAjax = Class.create();
CopyVariablesAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getVariableJson: function() {
var ritmId = this.getParameter('sysparm_copy_from_ritm_id');
var ritm = new GlideRecord('sc_req_item');
if(!ritm.get(ritmId)) {
return "{}";
}
return new CatalogVariableURLBuilder().getJsonFromGr(ritm);
},
type: 'CopyVariablesAjax'
});
Ok now you just need to, add the variable set to any catalog item that you want this functionality to work in, and also you should add the widget to the ticket page where the button needs to be.

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎03-24-2021 08:22 AM
Ok so yeah its like this widget to resubmit request on the ticket page, script include to build the URL, variable set called 'copy_variables_from_uri_ritm_id' with client script to parse the URL
Widget HTML:
<div ng-if="::c.data.show_button" class="panel panel-default b wrapper-md" ng-class="{'wrapper-md': options.native_mobile != 'true', 'wrapper-sm': options.native_mobile == 'true'}">
<button name="resubmit" ng-disabled="c.resubmitting" ng-click="c.redirectToResubmit()" class="btn btn-primary btn-block ng-binding ng-scope">
Resubmit Request
</button>
</div>
Widget Client script:
function() {
var c = this;
c.resubmitting = false;
c.redirectToResubmit = function() {
c.resubmitting = true;
c.server.get({
resubmitting: true
}).then(function(r) {
top.window.location = r.data.resubmit_url;
});
}
}
Widget Server script:
(function() {
var tableName = $sp.getParameter('table');
var recordId = $sp.getParameter('sys_id');
if(tableName && recordId) {
var recordGr = new GlideRecord(tableName);
recordGr.get(recordId);
if(!input) {
data.show_button = new CatalogVariableURLBuilder().hasVariableSet(recordGr, 'copy_variables_from_uri_ritm_id');
}
}
if(input && input.resubmitting) {
data.resubmit_url = new CatalogVariableURLBuilder().getResubmitURL('sc_cat_item', recordGr);
}
})();
CatalogVariableURLBuilder script include:
var CatalogVariableURLBuilder = Class.create();
CatalogVariableURLBuilder.prototype = {
initialize: function() {},
getJsonFromGr: function(ritmGr) {
var variablesObj = ritmGr.variables.getElements(false);
var outputObj = {};
for (var i = 0; i < variablesObj.length; ++i) {
var variable = variablesObj[i];
var question = variable.getQuestion();
if (question.getValue() && question.getName()) {
outputObj[question.getName()] = question.getValue();
}
}
return JSON.stringify(outputObj);
},
hasVariableSet: function(ritmGr, variableSetName) {
if(ritmGr.getTableName() != 'sc_req_item' || !ritmGr.getValue('cat_item')) {
return false;
}
var ioSetItem = new GlideRecord('io_set_item');
ioSetItem.addQuery('sc_cat_item', ritmGr.getValue('cat_item'));
ioSetItem.addQuery('variable_set.internal_name', variableSetName);
ioSetItem.query();
return ioSetItem.hasNext();
},
getResubmitURL: function(catItemPageId, ritmGr) {
var catItemSysId = ritmGr.cat_item.toString();
return "?id=" + catItemPageId + "&sys_id=" + catItemSysId + "&sysparm_copy_from_ritm_id=" + ritmGr.getUniqueValue();
},
type: 'CatalogVariableURLBuilder'
};
Variable set:
Onload client script (in this variable set)
function onLoad() {
var prop, propVal;
//get parameter from URL
var ritmId = getParameterValue('sysparm_copy_from_ritm_id');
console.debug('sysparm_variables URI parameter value: ' + ritmId);
if (!ritmId) {
//If parameter is empty, stop here.
console.debug('aborting due to a missing sysparm_copy_from_ritm_id URI parameter');
return;
}
var getVariableJson = new GlideAjax('CopyVariablesAjax');
getVariableJson.addParam('sysparm_name', 'getVariableJson');
getVariableJson.addParam('sysparm_copy_from_ritm_id', ritmId);
getVariableJson.getXMLAnswer(populateForm);
function populateForm(variableJsonStr) {
console.debug('sysparm_copy_from_ritm_id variables JSON object: ' + variableJsonStr);
var variableJson = JSON.parse(variableJsonStr);
for(var formVariable in variableJson) {
if(g_form.hasField(formVariable)) {
g_form.setValue(formVariable, variableJson[formVariable]);
}
}
}
}
function getParameterValue(name) {
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
//Check if portal (gel=undefined) or classic UI, and get the HREF data appropriately in either case.
var hrefPath = (typeof gel == 'undefined') ? this.location.href : top.location.href;
var results = regex.exec(hrefPath);
console.debug('HREF: ' + hrefPath);
if (results == null) {
return "";
} else {
return unescape(results[1]);
}
}
CopyVariablesAjax script include client callable:
var CopyVariablesAjax = Class.create();
CopyVariablesAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getVariableJson: function() {
var ritmId = this.getParameter('sysparm_copy_from_ritm_id');
var ritm = new GlideRecord('sc_req_item');
if(!ritm.get(ritmId)) {
return "{}";
}
return new CatalogVariableURLBuilder().getJsonFromGr(ritm);
},
type: 'CopyVariablesAjax'
});
Ok now you just need to, add the variable set to any catalog item that you want this functionality to work in, and also you should add the widget to the ticket page where the button needs to be.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎03-24-2021 11:57 AM
Thanks, Chris! This is one of the most complete answers I have ever gotten in such a short amount of time. It is almost perfect.
I do have one field (end date) that is not populating in the particular request item that this was originally requested for. It is running up against a catalog client script on the item that validates that the end date is not before the start date so that when the new request is created it gives the user the warning that desired end date has to be a date in the future and they have to fill in the end date.
I would also like it to be so that the user cannot use that button (maybe with a message that they can only resubmit closed requests) or it does not appear unless the original request is in closed state. Any idea how I might accomplish that?

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎03-24-2021 12:22 PM
Ok so yeah it's in the server script where its like
data.show_button = new CatalogVariableURLBuilder().hasVariableSet(recordGr, 'copy_variables_from_uri_ritm_id');
Just replace with
data.show_button = new CatalogVariableURLBuilder().hasVariableSet(recordGr, 'copy_variables_from_uri_ritm_id') && recordGr.active;
Can you share the client script for the date validation maybe I can help with that
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎03-24-2021 12:45 PM
Thanks, Chris. I actually had to change it to
data.show_button = new CatalogVariableURLBuilder().hasVariableSet(recordGr, 'copy_variables_from_uri_ritm_id') && recordGr.state == '3';
to get it the button to only show when the request is closed, but you pointed me in the right direction.
Below is the code for the date validation. I also noticed as I was trying to troubleshoot that when the validation message comes up and I click okay, then go down to the start date the start date is one day earlier than on the original request. When I turn off the validation script the start and end dates both populate, but one day earlier than on the original request.
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == '') {
return;
}
if (g_form.getValue('start_date') != '') {
var dFormat = g_user_date_format;
var tFormat = g_user_date_time_format;
var end = getDateFromFormat(g_form.getValue('end_date'), dFormat);
var today = getDateFromFormat(formatDate(new Date(), dFormat), dFormat);
var sDate = getDateFromFormat(g_form.getValue('start_date'), dFormat);
if (today > end) {
alert(getMessage("Desired End Date has to be a date in the future"));
g_form.setValue('end_date', '');
} else if (sDate != '' && end < sDate) {
alert(getMessage("End date should not be before Start date"));
g_form.setValue('end_date', '');
}
} else {
g_form.setValue('end_date', '');
alert(getMessage('First please select Start Date'));
}
}