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 => { }); });