diff --git a/CHANGELOG.md b/CHANGELOG.md
index fcf45b6..a6af1e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# CHANGELOG.md
-## 8.2.3.4 (2024-11-02)
+
+## 8.2.5.0 (2024-11-09)
+Features:
+ - Add /sns command to open the new ServiceNow Studio (LinkedIn post #philgoesdeep)
+ - Add /ois command to open current record in existing ServiceNow Studio as tab, as a tab (Beta)
+ - Inside ServiceNow Studio slash command inline results, will now open the record as a tab inside ServiceNow studio when it is metadata. By holding a modifier key a new tab will open.
+ - Dynamic slash commands that search metadata tables, now show sys_name as first column in the inline results.
+
+Fixes / changes:
+ - Adjustments to override CTRL-/ and CMD-/ in ServiceNow Studio when the prioritize setting is enabled. A subsequent press of the shortcut will show the SNS search modal.
+ - Adjusted function snuResolveVariables to support commands like /vd in SNS
+ - Additional fix for link type switches (-t, -erd) to open without a trailing space in the slash command (Slack)
+ - Rename snuUpdateSettingsEvent to snuUpdateSettingsEvent
+
+## 8.2.3.4 (2024-11-05)
Fixes / changes:
- Fix accidentily renamed file tsWorker.js
- Initialize snusettings object in settingseditor.js, when it is not found in extenson storage (Reddit)
diff --git a/background.js b/background.js
index cb0c151..0a61f8d 100644
--- a/background.js
+++ b/background.js
@@ -157,7 +157,7 @@ chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
"instaceTagConfig": instaceTagConfig,
};
chrome.tabs.sendMessage(sender.tab.id, {
- "method": "snuUpdateSettingsEvent",
+ "method": "snuProcessEvent",
"detail": details,
}).then(response => {
console.log("Response from content script:", response);
@@ -219,6 +219,32 @@ chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
if (tabIndex > -1) createObj.index = tabIndex+1;
chrome.tabs.create(createObj);
}
+ else if (message.event == "openTabInStudio") {
+ chrome.tabs.query({}, (tabs) => {
+ // Loop through all open tabs
+ for (const tab of tabs) {
+ try {
+ if ((tab?.url || "").startsWith('http')){
+ const url = new URL(tab.url);
+ if (url.pathname.startsWith('/now/servicenow-studio/') &&
+ sender.tab.url.startsWith(url.origin)) {
+ chrome.tabs.sendMessage(tab.id, {
+ "method": "snuProcessEvent",
+ "detail": {
+ "action": "openTabInStudio",
+ "payload" : message.command
+ }
+ });
+ chrome.tabs.update(tab.id, { active: true });
+ break; // Exit the loop after finding the first match
+ }
+ }
+ } catch (e) {
+ console.error('Error parsing URL:', e);
+ }
+ }
+ });
+ }
return true;
});
diff --git a/content_script_parent.js b/content_script_parent.js
index 3e3ac40..78469e2 100644
--- a/content_script_parent.js
+++ b/content_script_parent.js
@@ -11,7 +11,7 @@ setTimeout(() => { //be sure content_script_all_frames.js is loaded first
},200);
-//attach event listener from popup
+//attach event listener from popup or background script
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
if (request.method == "getSelection"){
sendResponse({ selectedText: getSelection() });
@@ -37,7 +37,7 @@ chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
toggleSearch();
return true;
}
- else if (request.method == "snuUpdateSettingsEvent") { //pass settings to page
+ else if (request.method == "snuProcessEvent") { //pass event payload to page
if (typeof cloneInto != 'undefined') request = cloneInto(request, document.defaultView); //required for ff
var event = new CustomEvent(
request.method, request
diff --git a/inject.js b/inject.js
index 3fa3d1b..c146658 100644
--- a/inject.js
+++ b/inject.js
@@ -253,6 +253,10 @@ var snuslashcommands = {
"url": "/$studio.do",
"hint": "Open Studio"
},
+ "sns": {
+ "url": "/now/servicenow-studio",
+ "hint": "ServiceNow Studio"
+ },
"shortcut": {
"url": "//sa",
"hint": "Special slashcommand, accessible via extension keyboard shortcut"
@@ -347,6 +351,10 @@ var snuslashcommands = {
"url": "*",
"hint": "View data of current record (-p for popup)"
},
+ "ois": {
+ "url": "*",
+ "hint": "Open record in open ServiceNow Studio tab (Beta)"
+ },
"wf": {
"url": "/workflow_ide.do?sysparm_nostack=true",
"hint": "Workflow Editor"
@@ -416,15 +424,18 @@ var snuOperators = ["%", "^", "=", ">", "<", "ANYTHING", "BETWEEN", "DATEPART",
if (typeof g_ck == 'undefined') g_ck = null; //prevent not defined errors when not provided in older instances , also see #453
-document.addEventListener('snuUpdateSettingsEvent', e => {
- if (e.type == "snuUpdateSettingsEvent") {
+document.addEventListener('snuProcessEvent', e => {
+ if (e.type == "snuProcessEvent") {
if (e?.detail?.action == "updateSlashCommand") {
snuslashcommands[e.detail.cmdname] = e.detail.cmd;
snuSlashCommandShow('/' + e.detail.cmdname + ' ', 0);
}
else if (e?.detail?.action == "updateInstaceTagConfig") { //update instance tag settings hanlded in instancetag.js
if (typeof snuReceiveInstanceTagEvent == 'function')
- snuReceiveInstanceTagEvent(e);
+ snuReceiveInstanceTagEvent(e);
+ }
+ else if (e?.detail?.action == "openTabInStudio") { //update instance tag settings hanlded in instancetag.js
+ snuOpenTabInStudio(e);
}
}
@@ -460,6 +471,15 @@ if (typeof jQuery != "undefined") {
}
flowDesignerDoubleClick();
+function snuOpenTabInStudio(e) {
+ console.log('snuOpenTabInStudio', e);
+ let snsDispatchHook = window.top?.querySelectorShadowDom?.querySelectorDeep('sn-udc-data-provider'); //use this element to know we are in SNS and use it to dispatch the open record event
+ if (!snsDispatchHook) return;
+ snsDispatchHook.dispatch("STUDIO_SHELL_TABS#OPEN_TAB", {
+ "file": e?.detail?.payload
+ });
+}
+
function snuEncodeHtml(str) {
return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/');
}
@@ -525,13 +545,15 @@ function snuGetDirectLinks(targeturl, shortcut) {
snuslashcommands[shortcut].fields
var url = "api/now/table/" + targeturl.replace("_list.do", "") +
"&sysparm_display_value=true&sysparm_exclude_reference_link=true&sysparm_suppress_pagination_header=true&sysparm_limit=20" +
- "&sysparm_fields=sys_id," + fields;
+ "&sysparm_fields=sys_id,sys_name," + fields;
try {
var table = url.match(/.*\/(.*)\?/)[1]
} catch (ex) {
return false;
}
+ let snsDispatchHook = window.top?.querySelectorShadowDom?.querySelectorDeep('sn-udc-data-provider'); //use this element to know we are in SNS and use it to dispatch the open record event
+
snuFetchData(g_ck, url, null, jsn => {
var directlinks = '';
if (jsn.hasOwnProperty('result')) {
@@ -540,7 +562,12 @@ function snuGetDirectLinks(targeturl, shortcut) {
if (results.length == 0) directlinks = `No results found`;
var idx = 0;
var dispIdx = 0;
+
Object.entries(results).forEach(([key, val]) => {
+
+ if (fields == "sys_updated_on,sys_updated_by" && val?.sys_name)
+ fields = "sys_name,sys_updated_on"; //Use sys_name if identified as metadata
+
var fieldArr = fields.replace(/ /g, '').split(',');
var txtArr = [];
for (var i = 0; i < fieldArr.length && i < 2; i++) {
@@ -548,6 +575,7 @@ function snuGetDirectLinks(targeturl, shortcut) {
}
var txt = txtArr.join(' | ');
var link = table + ".do?sys_id=" + val.sys_id;
+ let metadataInSnsClass = (val?.sys_name && snsDispatchHook && !overwriteurl) ? 'class="snsopenrecord"' : '';
var target = "gsft_main"
if (overwriteurl) {
link = overwriteurl.replace(/\$sysid/g, val.sys_id);
@@ -567,7 +595,7 @@ function snuGetDirectLinks(targeturl, shortcut) {
dispIdx = '>';
idattr = '';
}
- directlinks += '' + dispIdx + ' ' + txt + '
';
+ directlinks += `${dispIdx} ${txt}
`;
});
if (directlinks.length > 50)
directlinks += `Tip: Hit SHIFT to toggle keyboard navigation
Results: ${jsn.resultcount}
`;
@@ -576,8 +604,23 @@ function snuGetDirectLinks(targeturl, shortcut) {
else {
directlinks = `No access to data`;
}
- window.top.document.getElementById('snudirectlinks').innerHTML = DOMPurify.sanitize(directlinks, { ADD_ATTR: ['target'] });
- window.top.document.getElementById('snudirectlinks');
+ let snudirectlinks = window.top.document.getElementById('snudirectlinks')
+ snudirectlinks.innerHTML = DOMPurify.sanitize(directlinks, { ADD_ATTR: ['target'] });
+ snudirectlinks.querySelectorAll('a.snsopenrecord').forEach(elm => {
+ elm.addEventListener('click', function (e) {
+ if (e.shiftKey || e.metaKey || e.ctrlKey) return;
+ const href = e.currentTarget.getAttribute('href');
+ const match = href.match(/([^\/?]+)\.do\?sys_id=([a-f0-9]{32})/);
+ const payload = match ? { table: match[1], sysId: match[2] } : null;
+
+ if (!payload) return;
+ e.preventDefault();
+ snsDispatchHook.dispatch("STUDIO_SHELL_TABS#OPEN_TAB", {
+ "file": payload
+ })
+ });
+ });
+
window.top.document.querySelectorAll("#snudirectlinks a").forEach(elm => {
if (elm.hash.startsWith('#snu:')) {
@@ -783,7 +826,7 @@ function snuSlashCommandAddListener() {
switchValue = snuslashswitchesvalueoverwrites[`${prop}.${tableName}`];
}
query = query.replace(val, "");
- if (snuslashswitches[prop].type == "link" && (snufilter + thisKeyWithSpace).includes("-" + prop)) {
+ if (snuslashswitches[prop].type == "link" && ((snufilter + thisKeyWithSpace).includes("-" + prop + " ") || snufilter.endsWith("-" + prop))) {
targeturl = switchValue.replace(/\$0/g, tableName);
targeturl = targeturl.replace(/\$sysid/, mySysId);
targeturl = snuResolveVariables(targeturl).variableString;
@@ -820,6 +863,10 @@ function snuSlashCommandAddListener() {
targeturl = snuResolve$(targeturl, query, e);
+
+ if (targeturl.includes("sysparm_query=") && !targeturl.includes("ORDERBY"))
+ targeturl += "^ORDERBYDESCsys_updated_on";
+
if (e.key == 'ArrowRight' || (e.key == 'Enter' && inlineOnly && !(e.ctrlKey || e.metaKey))) {
snuSlashLog(true);
snuGetDirectLinks(targeturl, shortcut);
@@ -953,6 +1000,30 @@ function snuSlashCommandAddListener() {
}
return;
}
+ else if (shortcut == "ois") {
+ let data = {};
+ let vars = snuResolveVariables("");
+
+ if (vars.tableName && vars.sysId){
+ data.table = vars.tableName;
+ data.sysId = vars.sysId;
+ let event = new CustomEvent(
+ "snutils-event",
+ {
+ detail: {
+ event: "openTabInStudio",
+ command: data
+ }
+ }
+ );
+ window.top.document.dispatchEvent(event);
+ snuSlashCommandHide();
+ }
+ else{
+ snuSlashCommandInfoText("No table name and sys_id found...",false)
+ }
+ return;
+ }
else if (shortcut == "copycells" || shortcut == "copycolumn") {
snuCopySelectedCellValues(query, shortcut);
snuSlashCommandHide();
@@ -1629,6 +1700,15 @@ function snuResolveVariables(variableString){
variableString = variableString.replace(/\$table/g,tableName);
variableString = variableString.replace(/\$sysid/g,sysId);
}
+ else if (location.pathname.startsWith("/now/servicenow-studio")){ //ServiceNow Studio
+ const decodedUrl = decodeURIComponent(location.href);
+ const urlObj = new URL(decodedUrl);
+ const params = new URLSearchParams(urlObj.search);
+ tableName = (params.get("table") || "").replace(/[^a-zA-Z0-9_]/g, "");
+ sysId = (params.get("sysId") || "").replace(/[^a-fA-F0-9]/g, "");
+ variableString = variableString.replace(/\$table/g,tableName);
+ variableString = variableString.replace(/\$sysid/g,sysId);
+ }
else { ///get sysid and tablename from portal or workspace or workflow studio
let searchParams = new URLSearchParams(window.location.search)
tableName = (searchParams.get('table') || searchParams.get('tableName') || searchParams.get('id') || '').replace(/[^a-z0-9-_]/g, '');
@@ -1694,7 +1774,7 @@ function snuSettingsAdded() {
snusettings.codeeditor ??= true;
snusettings.s2ify ??= true;
snusettings.highlightdefaultupdateset ??= true;
- snusettings.slashpopuppriority ??= false;
+ snusettings.slashpopuppriority ??= true;
snusettings.slashnavigatorsearch ??= true;
snusettings.slashhistory ??= 50;
snusettings.addtechnicalnames ??= false;
@@ -3494,7 +3574,7 @@ function snuSetShortCuts() {
}
snuSlashCommandAddListener();
- document.addEventListener("keydown", function (event) {
+ document.addEventListener("keydown", (event) => {
// const shortcuts = {
// 'Ctrl+Shift+p': function () {
@@ -3521,13 +3601,25 @@ function snuSetShortCuts() {
if (event.key == '/') {
if (snusettings.slashpopuppriority && (event?.target?.id !== 'snufilter' ||
(event?.target?.id == 'snufilter' && event?.target?.value.length > 1))) {
- if (!window.top?.querySelectorShadowDom?.querySelectorDeep('now-modal.keyboard-shortcuts-modal')){ //allow hidding when visible
+ if (!window.top?.querySelectorShadowDom?.querySelectorDeep('now-modal.keyboard-shortcuts-modal, sn-udc-unified-search div.is-expanded')){ //allow hidding when visible
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
event.stopPropagation();
+ setTimeout(() => {
+ let snsSearch = window.top?.querySelectorShadowDom?.querySelectorDeep('sn-udc-unified-search');
+ if (snsSearch && window.top?.snufilter) {
+ snsSearch.dispatch('SN_UDC_SEARCH#SET_SEARCH_PANE_VISIBILITY',{ "show": false });
+ window.top.snufilter.focus();
+ window.top.snufilter.setSelectionRange(1,1);
+ }
+ }, 150);
}
}
- };
+ }
+ else if (snufilter?.value?.length === 1 && (event.metaKey || event.ctrlKey)) {
+ snuSlashCommandHide();
+ return;
+ }
if (snusettings.slashoption == 'off') return;
let eventPath = event.path || (event.composedPath && event.composedPath());
diff --git a/js/sidepanel.js b/js/sidepanel.js
index 66c1d8c..725e78b 100644
--- a/js/sidepanel.js
+++ b/js/sidepanel.js
@@ -55,7 +55,7 @@ document.addEventListener('DOMContentLoaded', function () {
//run after initialized, to trigger repositioning after the sidepanel showed.
chrome.tabs.sendMessage(tabId, {
- "method": "snuUpdateSettingsEvent",
+ "method": "snuProcessEvent",
"detail": {
"action": "updateInstaceTagConfig",
"instaceTagConfig": snuInstanceTagConfig,
@@ -84,7 +84,7 @@ document.addEventListener('DOMContentLoaded', function () {
setToChromeSyncStorage("instancetag", snuInstanceTagConfig);
chrome.tabs.sendMessage(tabId, {
- "method": "snuUpdateSettingsEvent",
+ "method": "snuProcessEvent",
"detail": {
"action": "updateInstaceTagConfig",
"instaceTagConfig": snuInstanceTagConfig,
diff --git a/manifest.json b/manifest.json
index 338bb22..3c4f8bd 100644
--- a/manifest.json
+++ b/manifest.json
@@ -3,7 +3,7 @@
"short_name": "SN Utils",
"description": "Productivity tools for ServiceNow. (Personal work, not affiliated to ServiceNow)",
"author": "Arnoud Kooi / arnoudkooi.com",
- "version": "8.2.3.4",
+ "version": "8.2.5.0",
"manifest_version": 3,
"permissions": [
"activeTab",
diff --git a/popup.js b/popup.js
index 7f2fbff..f136302 100644
--- a/popup.js
+++ b/popup.js
@@ -1403,7 +1403,7 @@ function getSlashcommands() {
}
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, {
- "method" : "snuUpdateSettingsEvent",
+ "method" : "snuProcessEvent",
"detail" : details },
response => { });
});