Netsuite: how to add a custom link to the Nav Bar or Header - netsuite

Is there any way to customize the Nav Bar or the Header to have a custom link?
The use-case is that I have a JIRA issue collector that is driven by javascript. I would like the user to provide feedback from the page they are having issues. However, any solution I can come up with so far takes the user away from the current page.
Example of what I have that takes the user away:
I currently have a Suitelet that is in one of the menus. That Suitelet invokes javascript but even then the user is taken away.
I have a workflow on the case record that calls some Javascript Javascript in one of the UI-based action's conditions is invoked. Similar to #1 but on the case record.
I'm thinking I'm going to need to create and public a chrome extension for my company's domain just to get a pervasive bit of javascript to run for all pages...seems like a sledgehammer.

I hope someone can prove me wrong, but as far as I am aware there is no way to natively inject Javascript or anything into the NetSuite header/navbar - they don't offer customisation to the header/navbar.
I've resorted to creating a Userscript that I load through the Violent Monkey extension for Chrome or Firefox.
Example Userscript Template
// ==UserScript==
// #name NetSuite Mods (Example)
// #namespace Violentmonkey Scripts
// #match *.netsuite.com/*
// #include *.netsuite.com/*
// #grant GM_addStyle
// #version 1.0
// #author Kane Shaw - https://stackoverflow.com/users/4561907/kane-shaw
// #description 6/11/2020, 6:25:20 PM
// ==/UserScript==
// Get access to some commonly used NLAPI functions without having to use "unsafeWindow.nlapi..." in our code
// You can add more of these if you need access to more of the functions contained on the NetSuite page
nlapiSetFieldText = unsafeWindow.nlapiSetFieldText;
nlapiSetFieldValue = unsafeWindow.nlapiSetFieldValue;
nlapiGetFieldText = unsafeWindow.nlapiGetFieldText;
nlapiGetFieldValue = unsafeWindow.nlapiGetFieldValue;
nlapiSearchRecord = unsafeWindow.nlapiSearchRecord;
nlobjSearchFilter = unsafeWindow.nlobjSearchFilter;
nlapiLookupField = unsafeWindow.nlapiLookupField;
nlapiLoadRecord = unsafeWindow.nlapiLoadRecord;
nlapiSubmitRecord = unsafeWindow.nlapiSubmitRecord;
GM_pageTransformations = {};
/**
* The entrypoint for our userscript
*/
function GM_main(jQuery) {
// We want to execute these on every NetSuite page
GM_pageTransformations.header();
GM_pageTransformations.browsertitle();
// Here we build a function name from the path (page being accessed on the NetSuite domain)
var path = location.pathname;
if(path.indexOf('.')>-1) path = path.substr(0,path.indexOf('.'));
path = toCamelCase(path,'/');
// Now we check if a page "GM_pageTransformations" function exists with a matching name
if(GM_pageTransformations[path]) {
console.log('Executing GM_pageTransformations for '+path);
GM_pageTransformations[path]();
} else {
console.log('No GM_pageTransformations for '+path);
}
}
/**
* Changes the header on all pages
*/
GM_pageTransformations['header'] = function() {
// For example, lets make the header background red
GM_addStyle('#ns_header, #ns_header * { background: red !important; }');
}
/**
* Provides useful browser/tab titles for each NetSuite page
*/
GM_pageTransformations['browsertitle'] = function() {
var title = jQuery('.uir-page-title-secondline').text().trim();
var title2 = jQuery('.uir-page-title-firstline').text().trim();
var title3 = jQuery('.ns-dashboard-detail-name').text().trim();
if(title != '') {
document.title = title+(title2 ? ': '+title2 : '')+(title3 ? ': '+title3 : '');
} else if(title2 != '') {
document.title = title2+(title3 ? ': '+title3 : '');
} else if(title3 != '') {
document.title = title3;
}
}
/**
* Changes app center card pages (dashboard pages)
*/
GM_pageTransformations['appCenterCard'] = function() {
// For example, lets make add a new heading text on all Dashboard pages
jQuery('#ns-dashboard-page').prepend('<h1>My New Dashboard Title</h1>');
}
/**
* Convert a given string into camelCase, or CamelCase
* #param {String} string - The input stirng
* #param {String} delimter - The delimiter that seperates the words in the input string (default " ")
* #param {Boolean} capitalizeFirstWord - Wheater or not to capitalize the first word (default false)
*/
function toCamelCase(string, delimiter, capitalizeFirstWord) {
if(!delimiter) delimiter = ' ';
var pieces = string.split(delimiter);
string = '';
for (var i=0; i<pieces.length; i++) {
if(pieces[i].length == 0) continue;
string += pieces[i].charAt(0).toUpperCase() + pieces[i].slice(1);
}
if(!capitalizeFirstWord) string= string.charAt(0).toLowerCase()+string.slice(1);
return string;
}
// ===============
// CREDIT FOR JQUERY INCLUSION CODE: Brock Adams # https://stackoverflow.com/a/12751531/4561907
/**
* Check if we already have a local copy of jQuery, or if we need to fetch it from a 3rd-party server
*/
if (typeof GM_info !== "undefined") {
console.log("Running with local copy of jQuery!");
GM_main(jQuery);
}
else {
console.log ("fetching jQuery from some 3rd-party server.");
add_jQuery(GM_main, "1.9.0");
}
/**
* Add the jQuery into our page for our userscript to use
*/
function add_jQuery(callbackFn, jqVersion) {
var jqVersion = jqVersion || "1.9.0";
var D = document;
var targ = D.getElementsByTagName ('head')[0] || D.body || D.documentElement;
var scriptNode = D.createElement ('script');
scriptNode.src = 'https://ajax.googleapis.com/ajax/libs/jquery/'
+ jqVersion
+ '/jquery.min.js'
;
scriptNode.addEventListener ("load", function () {
var scriptNode = D.createElement ("script");
scriptNode.textContent =
'var gm_jQuery = jQuery.noConflict (true);\n'
+ '(' + callbackFn.toString () + ')(gm_jQuery);'
;
targ.appendChild (scriptNode);
}, false);
targ.appendChild (scriptNode);
}
You can copy and paste that code as-is into a new Userscript and it will do the following:
Make Browser tabs/windows have useful titles (shows order numbers, customer names, vendor names etc - not just "Sales Order")
Change the header background to red (as an example)
Add a new heading to the top of all "Dashboard" pages that says "My New Dashboard Title" (as an example)

Related

On an afterSubmit when we creating a copy of one inventory item (name with '-c') ,The original ID of item link should come in a field on a copy order

I tried this above, here I am getting a null value only from my previous record.
Kindly give some guidance to solve my questions.
thanks in advance.
/**
*#NApiVersion 2.0
*#NScriptType UserEventScript
*/
define(["N/url", "N/record", "N/runtime"], function (url, record, runtime) {
function afterSubmit(context){
var recordobj = context.newRecord;
var prevItemrecord= context.oldRecord;
var Itemname = recordobj.getValue({fieldId:'itemid'});
var prevItemname = prevItemrecord.getValue({fieldId : 'itemid'});
var Type=context.type;
var checkbox=recordobj.getValue({fieldId:'custitem17'});
if(Type== context.UserEventType.CREATE)
if((Itemname=prevItemname+'-c')&&(checkbox=true))
record.submitFields({
type: recordobj.type,
id: recordobj.id,
values:{custitem_item_link:prevItemname}
});
}
return{
afterSubmit:afterSubmit
}
});
This is my code
On create there is no old record.
Since you are trying to update the same record as was just created you are better off having this in a beforeSubmit event script
if((Itemname=prevItemname+'-c')&&(checkbox=true)) this is an error
if((Itemname == prevItemname+'-c') && checkbox) is more what you need
If you are trying to capture a copy operation you can set that up in the beforeLoad event that you use in the beforeSubmit event.
function beforeLoad(ctx){
if(ctx.type == ctx.UserEventType.COPY){
if(ctx.form){
ctx.form.addField({
id:'custpage_original_item',
label:'Copied Item',
type:ui.FieldType.SELECT,
source:'item'
}).updateDisplayType({
displayType:ui.FieldDisplayType.HIDDEN
}).defaultValue = ctx.request.parameters.id;
// your naming makes me wonder if you are trying to link to the
// source item rather than just saving a reference to the source
// item's name
/*
* Using the original item's name like below is closer to what you
* posted but I think by the time the script runs the itemid field
* has been cleared.
* ctx.form.addField({
* id:'custpage_original_item_name',
* label:'Copied Item Name',
* type:ui.FieldType.TEXT
* }).updateDisplayType({
* displayType:ui.FieldDisplayType.HIDDEN
* }).defaultValue = ctx.newRecord.getValue({fieldId:'itemid'});
*/
}
}
}
function beforeSubmit(ctx){
if(ctx.type == ctx.UserEventType.CREATE){
const itemRec = ctx.newRecord;
if(itemRec.getValue({fieldId:'custitem17'})){
const sourceId = itemRec.getValue({fieldId:'custpage_original_item'})
if(sourceId){
itemRec.setValue({
fieldId:'custitem_item_link:prevItemname',
value:sourceId
});
/* or use a search function to look up the original item's name
* and then test the new item's name.
*/
}
}
}
}

the jscript code we have on our store website is not working properly

I have this code installed on my website and what it is suppose to do is see if a product has a single or double attribute and then it disables products from there that are out of stock so customers can't click on it. The code works great for the single attribute products but it doesn't seem to work for the double attribute products.
Single Attribute Product link - https://true-grit-running-company.shoplightspeed.com/goodr-sunglasses.html
Double Attribute Product link - https://true-grit-running-company.shoplightspeed.com/womens-bondi-7.html?id=65554420&quantity=1
`
// A quick check to see if it is a product being viewed (checking the microdata) - to avoid running the rest of the code if viewing a page other than the product page
if ($('[itemtype*="//schema.org/Product"]').length > 0) {
//Check the url to see if a variant is being viewed or not
var curl = location.href;
//choose the appropriate ajax url
if (curl.indexOf('?') > -1) {
var url = curl + '&format=json';
} else {
var url = '?format=json';
}
//Start the ajax call
$.ajax({
url: url,
})
// Add the disabled attribute to the variants that aren't available
.done(function(obj) {
//create a variable with the product variants
var data = obj.product.variants;
//fun a function on each variant
$.each(data, function(index, value) {
//check if any of the variants aren't available for purchase
if (!value.stock.available) {
//CODE FOR DOUBLE ATTRIBUTE VARIANTS
//check if the variants are double attribute
if (value.title.indexOf(',') > -1) {
console.log('Double Attribute matrix!');
var attribute1 = value.title.replace(/"/g,'').split(',')[0].split(": ")[1];
//only disable the variants for which the first attribute is being viewed
if ($('select[name*="matrix"]:first()').val() == attribute1) {
var option = value.title.replace(/"/g,'').split(',')[1].split(":")[1];
$('select[name*="matrix"] option:contains(' + option + ')').each(function(){
if ($(this).text() == option) {
$(this).attr('disabled', 'true');
}
});
}
//CODE FOR SINGLE ATTRIBUTE VARIANTS
} else {
console.log('Single Attribute matrix!');
var option = value.title.split(': ')[1];
var selectname = value.title.split(': ')[0];
$('select[name*="matrix"] option:contains(' + option + ')').each(function(){
if ($(this).text() == option) {
$(this).attr('disabled', 'true');
}
});
}
}
})
});
} else {
console.log('not a product page!');
}
`

Viewing file properties instead of file in SharePoint document library

I have a document library on SharePoint online with lots of columns for metadata. These columns won't fit in a single view on screen, so I want the users to first view the properties of the file before downloading them.
Is there a way to change the behavior of the SharePoint library ensure that the user views the file properties first when they click on the filename?
PS: I understand I could have used lists, but after loading about 10000 documents, I have decided to use it as a last resort. Thank you.
Custom the LinkFilename field by CSR.
Sample code:
(function () {
'use strict';
var CustomWidthCtx = {};
/**
* Initialization
*/
function init() {
CustomWidthCtx.Templates = {};
CustomWidthCtx.Templates.Fields = {
'LinkFilename': {
'View': customDisplay
}
};
// Register the custom template
SPClientTemplates.TemplateManager.RegisterTemplateOverrides(CustomWidthCtx);
}
/**
* Rendering template
*/
function customDisplay(ctx) {
var currentVal = '';
//from the context get the current item and it's value
if (ctx != null && ctx.CurrentItem != null)
currentVal = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
var el = "<div class='ms-vb itx' id='" + ctx.CurrentItem.ID + "' app='' ctxname='ctx37'><a title='" + ctx.CurrentItem._ShortcutUrl + "' class='ms-listlink ms-draggable' aria-label='Shortcut file' onfocus='OnLink(this)' href='/Doc4/Forms/EditForm.aspx?ID=" + ctx.CurrentItem.ID + "' target='_blank' DragId='17'>" + ctx.CurrentItem.FileLeafRef + "</a></div>";
// Render the HTML5 file input in place of the OOTB
return el;
}
// Run our intiialization
init();
})();

Select text between two bookmarks in Komodo Edit

Let's say I have the following (example) code in combined.js:
/* jQuery, Moment.js, Bootstrap, etc. */
Child.prototype.doSchool = function(data) { // Bookmarked
var essay = data.essay || {};
if (essay) {
var spelling = checkSpelling(essay, EN_US_GRADE_7);
return spelling.grade();
}
}
/* Extensive and Turing-complete code base */
var burt = new Child();
if (burt.doSchool({essay: "i like trains"}) < .65) burt.comfort(); // Bookmarked
/* jQuery extensions, Fallout 4, etc. */
The file is bookmarked in Komodo Edit 9.3.x in the locations marked by // inline comments.
Any /* block comments */ indicate thousands of lines of code.
The source between the bookmarks exists in another file, school.inc.js. I want to know if there is an easy way to select all the text between the bookmarks, so that combined.js can be easily updated by pasting the contents of school.inc.js over it without having to use a combining utility.
There is no built in way to do this but you could possible do it by writing a Userscript.
You'll want to use the Komodo Editor SDK.
// This assumes you're running the Userscript starting at the first bookmark
var editor = require("ko/editor");
var startSelect;
var endSelect;
var done = false;
function selectBookmarkRegion(){
if(editor.bookmarkExists()) { // check if bookmark is set on current line
startSelect = { // save it's line start
line: editor.getLineNumber(),
ch: 0
};
} else {
alert("Start me on a line with a Bookmark");
}
editor.goLineDown();
while(!done){
if(editor.bookmarkExists())
{
endSelect = {
line: editor.getLineNumber(),
ch: editor.getLineSize()
};// Save line end
done = true;
}
editor.goLineDown();
// found a bug as I was writing this. Will be fixed in the next releases
if (editor.getLineNumber() + 1 == editor.lineCount())
{
done = true;
}
}
editor.setSelection(startSelect, endSelect); // Wrap the selection
}
selectBookmarkRegion();

Can I allow the extension user to choose matching domains?

Can I allow the domain matching for my extension to be user configurable?
I'd like to let my users choose when the extension runs.
To implement customizable "match patterns" for content scripts, the Content script need to be executed in by the background page using the chrome.tabs.executeScript method (after detecting a page load using the chrome.tabs.onUpdated event listener).
Because the match pattern check is not exposed in any API, you have to create the method yourself. It is implemented in url_pattern.cc, and the specification is available at match patterns.
Here's an example of a parser:
/**
* #param String input A match pattern
* #returns null if input is invalid
* #returns String to be passed to the RegExp constructor */
function parse_match_pattern(input) {
if (typeof input !== 'string') return null;
var match_pattern = '(?:^'
, regEscape = function(s) {return s.replace(/[[^$.|?*+(){}\\]/g, '\\$&');}
, result = /^(\*|https?|file|ftp|chrome-extension):\/\//.exec(input);
// Parse scheme
if (!result) return null;
input = input.substr(result[0].length);
match_pattern += result[1] === '*' ? 'https?://' : result[1] + '://';
// Parse host if scheme is not `file`
if (result[1] !== 'file') {
if (!(result = /^(?:\*|(\*\.)?([^\/*]+))(?=\/)/.exec(input))) return null;
input = input.substr(result[0].length);
if (result[0] === '*') { // host is '*'
match_pattern += '[^/]+';
} else {
if (result[1]) { // Subdomain wildcard exists
match_pattern += '(?:[^/]+\\.)?';
}
// Append host (escape special regex characters)
match_pattern += regEscape(result[2]);
}
}
// Add remainder (path)
match_pattern += input.split('*').map(regEscape).join('.*');
match_pattern += '$)';
return match_pattern;
}
Example: Run content script on pages which match the pattern
In the example below, the array is hard-coded. In practice, you would store the match patterns in an array using localStorage or chrome.storage.
// Example: Parse a list of match patterns:
var patterns = ['*://*/*', '*exampleofinvalid*', 'file://*'];
// Parse list and filter(exclude) invalid match patterns
var parsed = patterns.map(parse_match_pattern)
.filter(function(pattern){return pattern !== null});
// Create pattern for validation:
var pattern = new RegExp(parsed.join('|'));
// Example of filtering:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (changeInfo.status === 'complete') {
var url = tab.url.split('#')[0]; // Exclude URL fragments
if (pattern.test(url)) {
chrome.tabs.executeScript(tabId, {
file: 'contentscript.js'
// or: code: '<JavaScript code here>'
// Other valid options: allFrames, runAt
});
}
}
});
To get this to work, you need to request the following permissions in the manifest file:
"tabs" - To enable the necessary tabs API.
"<all_urls>" - To be able to use chrome.tabs.executeScript to execute a content script in a specific page.
A fixed list of permissions
If the set of match patterns is fixed (ie. the user cannot define new ones, only toggle patterns), "<all_urls>" can be replaced with this set of permissions. You may even use optional permissions to reduce the initial number of requested permissions (clearly explained in the documentation of chrome.permissions).

Resources