Suitescript 2.0 addButton - netsuite

I have a suitelet with a sublist button and I am trying to get the button to execute a function on a custom module. I can not get it to work. I get an error "Cannot call method "receive" of undefined"
Any Ideas?
Snippet of code to add button
define(['N/error', 'N/record', 'N/search', 'N/ui/serverWidget','./lib1'],
function(error, record, search, ui, lib1) {
//... some code here
searchSublist.addButton({
id: 'custpage_recievepayment',
label: 'Receive Payment',
functionName: "lib1.receive()"});
}
Snippet of custom Module
define(['N/redirect'],
function(redirect){
function receive(){
var deal = '497774';
var url = redirect.toSuitelet({
scriptId: 'customscript_deal_entry_2_0',
deploymentId: 'customdeploy1',
returnExternalUrl: false,
params: {
prevdeal: url
}
})
}
});

I was able to get this to work with out the client script or exporting it to the global object that was suggested. The key was to modify my custom module to return the function I wanted to use on the button and calling the custom module file with form.clientScriptFileId
//suitelet
define(['N/error', 'N/record', 'N/search', 'N/ui/serverWidget'],
function(error, record, search, ui) {
// other code here
form.clientScriptFileId = 78627;//this is the file cabinet internal id of my custom module
var searchSublist = form.addSublist({
id: 'custpage_deals',
type: ui.SublistType.LIST,
label: 'Deals'
})
searchSublist.addButton({
id: 'custpage_receivepayment',
label: 'Receive Payment',
functionName: "receive()"
});
//custom module
define(['N/url','N/error'],
function(url, error) {
return{
receive: function(){
//function code here
}
}
})

For suitescript 2.0, you'll need to define the file that actually contains your function.
/**
*#NApiVersion 2.x
*#NScriptType UserEventScript
*/
define([],
function() {
function beforeLoad(context) {
if(context.type == "view") {
context.form.clientScriptFileId = 19181;
context.form.addButton({
id: 'custpage_dropshippo',
label: 'Generate Dropship PO',
functionName: 'generateDropshipPurchaseOrder'
});
}
}
return {
beforeLoad: beforeLoad,
};
}
);
In that example, the value 19181 is the file cabinet ID of the following file (which doesn't need a deployment but does need a script record):
/**
*#NApiVersion 2.x
*#NScriptType ClientScript
*/
define([],
function() {
function pageInit() {
}
function generateDropshipPurchaseOrder() {
console.log("foo");
}
return {
pageInit: pageInit,
generateDropshipPurchaseOrder: generateDropshipPurchaseOrder
};
});

Thanks for sharing this information. I finally made it to work by using the "file cabinet internal id" not the "client script" internal id, as mentioned by someone in this thread.
Also i noticed through the chrom developer tool that "scriptContext" is not automatically passed over to the client script function as i had expected. So i had to manually pass in the data i need in the client script through the following way:
function beforeLoad(scriptContext) {
var form = scriptContext.form;
form.clientScriptFileId = 33595032;
form.addButton({
id: 'custpage_reprocessinvoice',
label: 'Reprocess Invoice',
functionName: 'reprocess(' + scriptContext.newRecord.id + ')'
});
}
Hope this may help someone and save him/her a bit of time.

Button click handlers is something of an issue (at least, in my opinion) in 2.0. You are getting this error because lib1 is not defined on the client side.
What I have had to do is create a Client Script that exports my click handlers to the global object to make them accessible. Something like:
// Client script
define(['my/custom/module'], function (myModule) {
window.receive = myModule.receive;
});
// Suitelet
define(...
// some code...
searchSublist.addButton({
id: 'custpage_recievepayment',
label: 'Receive Payment',
functionName: "receive"
});
// other code...
It's not ideal, and certainly not good practice in general to export to the global space, but I do not know any other way to reference a function within a module on a button click.

After not getting this to work after multiple tries I filed a defect with Netsuite (Defect 390444) and they have just told me that this has now been fixed and tested and will be in the next major release.

Related

Why is the second Jest mock function never being called?

I am mocking navigator functions for simple clipboard functionality. Here is the relevant code:
// FUNCTION
/**
* Adds a click event to the button which will save a string to the navigator clipboard. Checks for
* clipboard permissions before copying.
*/
function loader(): void {
async function copyUrl(): Promise<void> {
const permission = await navigator.permissions.query({ name: "clipboard-write" });
if (permission.state == "granted" || permission.state == "prompt" ) {
await navigator.clipboard.writeText("the url");
} else {
console.error('Permission not supported');
}
}
const button = document.querySelector('button') as HTMLElement;
button.addEventListener('click', async () => {
await copyUrl();
});
}
// TEST
it('works', () => {
// mock navigator functions
Object.assign(navigator, {
permissions: {
query: jest.fn(async () => ({ state: "granted" }))
},
clipboard: {
writeText: jest.fn(async () => {})
}
});
// initialize DOM
document.body.innerHTML = '<button></button>';
loader(); // adds the event listener
// click the button!
const button = document.querySelector('button') as HTMLElement;
button.click();
expect(navigator.permissions.query).toHaveBeenCalledTimes(1);
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('the url');
});
The test fails on expect(navigator.clipboard.writeText).toHaveBeenCalledWith('the url') with:
Expected: "the url" Number of calls: 0
Defeats the purpose of permissions, yes, but for the sake of debugging:
Try adding a clipboard call before permissions call like so?
// FUNCTION
// ...
async function copyUrl(): Promise<void> {
// add this
await navigator.clipboard.writeText('the url');
// keep the rest still
const permission = await navigator.permissions.query({ name: "clipboard-write" });
// ...
}
This fails on the first assertion now, expect(navigator.permissions.query).toHaveBeenCalledTimes(1) with
Expected number of calls: 1 Received number of calls: 0
With the addition above, I also changed the assertions to be:
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('the url');
expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(2);
expect(navigator.permissions.query).toHaveBeenCalledTimes(1);
... which failed on the second assertion because it expected 2 calls but only received 1.
I have been testing in a VSCode devcontainer and tried out the extension firsttris.vscode-jest-runner to debug the test. With breakpoints in the loader function, I'm able to see that every single line executes perfectly with my mockup but still fails at the end of debug.
I even changed the mock navigator.permissions.query function to return { state: 'denied' } instead. Both running and debugging, it did not satisfy the permission check and gave an error to the console as expected but the test still failed at expect(navigator.permissions.query).toHaveBeenCalledTimes(1) (with the added writeText call before it).
It seems to me that after the first call of a mock function, the others just don't work.
Am I missing something? Send help pls lol
EDITS
Using jest.spyOn as in this answer has the same issues.
Using an async test with an expect.assertions(n) assertion still produces the exact same issue.

Suitescript 2.0 get the contents of a file from a Suitelet file field and use the data in a scheduled script

I have created a button on the work order records that opens a suitelet form with a fieldType.FILE to allow the user to choose a file locally. When the Suitlet is submitted I want the script to take the file, take the contents, and then pass two arrays to a scheduled script. So far I have no errors and none of my log.debugs are showing up in Netsuite.
Here is the suitelet code for reference:
/**
* #NApiVersion 2.x
* #NScriptType Suitelet
*
* #author MF
*/
define(['N/ui/serverWidget', 'N/file', 'N/search', 'N/email','N/ui/dialog'], function (serverWidget, file, search, email, dialog) {
function onRequest(context) {
//getWoData(context);
buildPage(context);
};
function buildPage(context) {
var woForm = serverWidget.createForm({
title: "Mass Update Work Order Due Dates",
})
woForm.addField({
id: 'custpage_wo_export',
label: "Work Order Update File",
type: serverWidget.FieldType.FILE
});
woForm.addSubmitButton({
id: 'custpage_submitwo',
label: 'Submit',
functionName: 'SubmitWorkOrders'
});
woForm.clientScriptModulePath = "SuiteScripts/WoSubmitClient.js"
context.response.writePage(woForm);
}
return {
onRequest: onRequest
};
});
The issue starts at the client as far as I know.
Here is the client script for the Suitelet above:
/**
* #NApiVersion 2.X
* #NScriptType ClientScript
* #NModuleScope SameAccount
* #author MF
*/
define(['N/url', 'N/https','N/currentRecord'], function (url, https, currentRecord) {
function pageInit(context) { };
function SubmitWorkOrders(context) {
var objRecord = currentRecord.get();
//Handle the save button
log.debug({
title: 'SubmitWorkOrders',
details: "This function was called"
})
var uploadedFile = objRecord.getValue({
fieldId: 'custpage_wo_export'
})
log.debug({
title: 'File',
details: uploadedFile.name
})
var taskCreate = url.resolveScript({
scriptId: 'customscript_scheduledscripttaskagent',
deploymentId: 'customdeploy_scheduledscripttaskagent',
returnExternalUrl: false,
params: {
input_file: uploadedFile
}
});
}
return {
pageInit: pageInit,
saveRecord: SubmitWorkOrders
};
});
This client script then calls another suitelet to create and run the task.
/**
* #NApiVersion 2.x
* #NScriptType Suitelet
*
* #author MF
*/
define([
'N/task',
'N/redirect',
'N/file'
], function (task, redirect, file) {
function scriptTask(context) {
//check file type
var input_file = context.request.params.input_file;
log.debug({
title: 'File Name',
details: input_file.name
})
var woUpdateScriptTask = task.create({
taskType: task.taskType.SCHEDULED_SCRIPT,
deploymentId: 'customscript_womassupdate',
params: { uploadedFile: uploadedFile },
scriptId: 715
}).submit();
}
return {
onRequest: scriptTask
};
});
Any advice would be appreciated, I have only been in the Netsuite realm for a few weeks and javascript in general only a bit longer than that. Note that there is some residual modules in some of these scripts from testing different approaches.
Currently the entire process looks something like this -> (1)UserEventScript on Work Orders -> (2)ClientScript on Work Orders -> (3)First attached Suitelet -> (4)ClientScript for Suitelet -> (5)Suitelet to create the task to run the scheduled script -> (6)ScheduledScript to update work orders. Looking to get some advice on getting the file contents from step 3 to step 5 so the data could get parsed before they are passed to the scheduled script.
Thanks in advance
I think this is how you can get the file contents from your first Suitelet to your Scheduled script:
First, you need to combine your 1st and 2nd Suitelet into one single Suitelet. This is required for getting selected file.
You can simply do it like this in your first Suitelet > function onRequest():
function onRequest(context) {
if (request.method == 'GET') { //This block will execute when you open your Suitelet form
buildPage(context);
}else{ //This block will execute when Submit button is clicked
scriptTask(context); // add scriptTask() function body in your 1st Suitelet from 2nd Suitelet
}
};
Second, if your client script is attached with a Suitelet you can view it's logs using console.log instead of log.debug in web browser's console (open with Ctrl+Shift+i). BUT getting value of 'custpage_wo_export' in your CS will only return a string like 'C:\fakepath\your_file_name.file_extension'. You might need to first save selected file in NetSuite's File Cabinet to access it's contents. This can be done in function scriptTask() now in your 1st Suitelet.
In Client Script > function SubmitWorkOrders(), use:
function SubmitWorkOrders(context) {
var objRecord = context.currentRecord;
var uploadedFile = objRecord.getValue({
fieldId: 'custpage_wo_export'
})
console.log('File', uploadedFile);
return true; //use return 'false' to view the log in console first
}
Now Third, in function scriptTask(), now in 1st Suitelet, use:
function scriptTask(context) {
if (context.request.files.custpage_wo_export) {
var fileObj = context.request.files.custpage_wo_export; //save this fileObj and then access it's contents.
log.debug('fileObj', fileObj);
fileObj.folder = 1630; //speficy the id of folder in File Cabinet
var fileId = fileObj.save();
log.debug("fileId", fileId);
//Now, load newly saved file and access it's contents. You can also access file contents in your Scheduled script depending on what you prefer/require.
if (fileId) {
var fileObj = file.load({
id: fileId
});
fileContents = fileObj.getContents();
log.debug("fileContents", fileContents);
/*
Now, you have fileId and/or fileContents. You can execute your Scheduled script here using task.create() etc.
*/
}
}
}
Note: Selecting and uploading/saving the same file in File Cabinet will simply overwrite the existing file keeping file id same.

Is there a way to make bots aware of what page they are on?

I have a chatbot that will eventually be deployed on multiple websites, and there are a number or variables that need to change based on the site (e.g. language, QnA Database, Dialog, etc.). I'd like to do this with a single bot, and just pass a variable so that it knows which page it is being rendered on (for a simple example, let's assume country pages: us, fr, de, etc.). I have been unsuccessful in passing this information to the bot.
Ideally this would be before the welcome message fires, but I can't even get it to send at all. I have a custom store set up:
const store = window.WebChat.createStore({}, function(dispatch) { return function(next) { return function(action) {
if (action.type === 'WEB_CHAT/SEND_MESSAGE') {
// Message sent by the user
PageTitleNotification.Off();
clearTimeout(interval);
} else if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY' && action.payload.activity.name !== "inactive") {
// Message sent by the bot
clearInterval(interval);
interval = setTimeout(function() {
// Change title to flash the page
PageTitleNotification.On('Are you still there?');
// Notify bot the user has been inactive
dispatch.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'inactive',
value: ''
}
});
}, 300000)
}
return next(action);
}}});
But for my use case I don't think what's in there actually matters, only that it is defined. The functions here just 1) clear an interval when the user sends a message and 2) set a new interval and send an inactivity message to the bot.
I also have a send message activity that is on a button click for a transcript. It looks like this:
document.querySelector('#transcriptButton').addEventListener('click', function() {
return store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: { text: 'Email me a transcript' }
});
/*return store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});*/
});
This sends a "front channel" message (that I can see in the bot) to request a transcript, which kicks off a dialog. That works. The commented out section alludes to what I'm trying to do. I have a separate dispatch statement as shown below, which has the exact same SEND_EVENT code as is commented out above. The SEND_EVENT does work as expected when it keys off the button click.
Here is the additional code I added. This is the piece that is NOT working. What I want is, when the bot has been rendered (but ideally before the welcome message), send this siteContext event to the bot so that I know where the bot is being rendered. I do not get any activity in the bot with this code. I also tried replacing it with SEND_MESSAGE instead of SEND_EVENT in a sort of reverse test from above, but that didn't work either.
// Test setting site context
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});
/*store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: {
text: 'eatonchatbot indexBackup.html'
}
});*/
It just occurred to me that this statement is probably running before the bot is rendered. So I put it in an interval and this DOES work. However, it does not fire the message until after the welcome message has been sent.
setTimeout(function() {
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});
}, 5000);
So this kind of works, but if this siteContext value was needed to determine the language of the welcome message, this would obviously fail. So my main ask here is, is there a better way to try to pass in a siteContext value like this, or is there some way to ensure that the context is received and can be used by the bot before the welcome message fires? I do see that there is a locale setting in the renderWebChat method, but I can't figure out if and how I could access that in the bot, and besides it may not be granular enough depending on the business need. But it seems if I could send some sort of value in that renderWebChat object, that might avoid all of the other crazy stuff I'm trying to do.
With some help from #Hessel and this issue I found on GitHub, I was able to come up with a solution. Just setting the values being passed in via onEvent (which I am now using in place of onTurn to reduce an if statement) isn't good enough if you need to alter content in the welcome message (e.g. language, user name, or an altogether different message). The onMembersAdded still fires before the values can be set, at least if you're setting them in userState. The key is to set up separate welcome messages in onEvent for directline and onMembersAdded for all other channels (I didn't include webchat as in the example as I'm not sending any event for that channel).
Here is the onEvent function I used:
this.onEvent(async (context, next) => {
// Check for inactivity
if (context.activity.name && context.activity.name === 'inactive') {
await context.sendActivity({
text: 'Are you still there? Is there anything else I can help you with?',
name: 'inactive'
});
}
// Check for webchat/join event (directline conversation initiation)
if (context.activity.name && context.activity.name === 'webchat/join') {
const userData = await this.userDialogStateAccessor.get(context, {});
userData.siteContext = context.activity.value;
// Debug
console.log(`The current language is: ${userData.siteContext.language}`);
console.log(`The current page is: ${userData.siteContext.page}`);
//await context.sendActivity(`The current language is: ${userData.siteContext.language}`);
//await context.sendActivity(`The current page is: ${userData.siteContext.page}`);
if (!userData.accountNumber) {
const dc = await this.dialogs.createContext(context);
await dc.beginDialog(AUTH_DIALOG);
await this.conversationState.saveChanges(context);
await this.userState.saveChanges(context);
} else {
if (context.activity.channelId == 'msteams') {
var welcomeCard = CardHelper.GetMenuCardTeams(welcomeMessage,'Y','Y');
} else {
var welcomeCard = CardHelper.GetMenuCard(welcomeMessage,'Y','Y');
}
await context.sendActivity(welcomeCard);
this.appInsightsClient.trackEvent({name:'conversationStart', properties:{accountNumber:userData.accountNumber}});
}
await this.userState.saveChanges(context);
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
The event I used in the custom store is pretty much the same as above, except I updated it to pull in the most preferred language and current url (was hard coded above).
store = window.WebChat.createStore({}, function (dispatch) {
return function (next) {
return function (action) {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join',
value: {
language: navigator.languages[0],
page: window.location.href
}
}
});
}
return next(action);
};
};
});
If you have your renderWebChat method inside a function that you can call so that your bot doesn't automatically start (I have a floating icon that causes the bot to load onclick) this should go outside that function.

Starting a dialog from conversationUpdate event in Node.js BotBuilder

I want to show the message and call a dialog when chatbot initialize. The below code shows the message. But, can not call a dialog.
bot.on('conversationUpdate', function (activity) {
// when user joins conversation, send welcome message
if (activity.membersAdded) {
activity.membersAdded.forEach(function (identity) {
if (identity.id === activity.address.bot.id) {
var reply = new builder.Message()
.address(activity.address)
.text("Hi, Welcome ");
bot.send(reply);
// bot.beginDialog("initialize", '/');
// session.beginDialog("initialize");
}
});
}});bot.dialog('/', intents);
Below is the code for dialog. I need to call below dialog when chatbot begins
bot.dialog('initialize', [
function (session, args, next) {
builder.Prompts.choice(session, "Do you have account?", "Yes|No", { listStyle: builder.ListStyle.button });
}, function (session, args, next) {
if (args.response.entity.toLowerCase() === 'yes') {
//session.beginDialog("lousyspeed");
session.send("No pressed");
} else if (args.response.entity.toLowerCase() === 'no') {
session.send("Yes pressed");
session.endConversation();
}
}]).endConversationAction("stop",
"",
{
matches: /^cancel$|^goodbye$|^exit|^stop|^close/i
// confirmPrompt: "This will cancel your order. Are you sure?"
});
I tried below methods. But it is not working
1. bot.beginDialog("initialize", '/');
2. session.beginDialog("initialize");
You are hitting this error because, although they have the same method name, the method signatures differ between session.beginDialog() and <UniversalBot>bot.beginDialog().
This can be a bit confusing since the first argument to session.beginDialog() is the dialogId, but when using bot.beginDialog() the first argument is the address, and the second param is the dialogId.
To solve this, call bot.beginDialog() with the correct input parameters as described in the SDK reference documentation - Eg. bot.beginDialog(activity.address, dialogId);
https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.universalbot.html#begindialog
You can also see the full method signature in the botbuilder.d TypeScript definition file here:
/**
* Proactively starts a new dialog with the user. Any current conversation between the bot and user will be replaced with a new dialog stack.
* #param address Address of the user to start a new conversation with. This should be saved during a previous conversation with the user. Any existing conversation or dialog will be immediately terminated.
* #param dialogId ID of the dialog to begin.
* #param dialogArgs (Optional) arguments to pass to dialog.
* #param done (Optional) function to invoke once the operation is completed.
*/
beginDialog(address: IAddress, dialogId: string, dialogArgs?: any, done?: (err: Error) => void): void;
I fixed my problem by using single line code
bot.beginDialog(activity.address, 'initialize');

XSP Partial Refresh

I am trying to send a value to server from anchor link and I call following code from a function which is called from the anchor link. Although I am able to trigger partial refresh,I get an error...any pointers please.....
var refreshId=dojo.query('[id$="testPanel"]')[0];
alert(refreshId.innerHTML)
alert(refreshId.id)
var mySubmitValue='whatYouWantToSendHere';
XSP.partialRefreshGet(refreshId, {
params: {
'$$xspsubmitvalue': mySubmitValue
},
onStart: function () {
alert('starting');
},
onComplete: function () {
alert('Complete');
},
onError:'myErrHandler( arguments[0], arguments[1] )'
});
You are sending the object to the server. Use the id of the element instead:
XSP.partialRefreshGet(refreshId.id, {

Resources