Improve my script run time - google-apps-script

Is there anyway to improve my script run time? I have a script that creates 2 listboxes: Listbox1 items is all my google site pages, listbox2 items is the sub-pages of listbox1 page. The script runs fine but sometimes it takes between 2 and 5 seconds to get all of the listbox2 items.
You can try my script here.
And here is my script:
function doGet()
{
var app = UiApp.createApplication();
//GUI with 2 listbox
//Listbox1: onclick > lbox1onclick(e), onchange > lbox1onchange(e)
app.add(app.loadComponent("MyUrlParser"));
var lbox1 = app.getElementById('ListBox1');
lbox1.addItem(' ');
var lbox1_Item = SitesApp.getSite('phichdaica').getChildByName('manga').getChildren();
for(var i = lbox1_Item.length-1; i >= 0; i--)
{
lbox1.addItem(lbox1_Item[i].getTitle());
}
return app;
}
function lbox1onclick(e)
{
var app = UiApp.getActiveApplication();
var lbox2 = app.getElementById('ListBox2');
lbox2.clear();
return app;
}
function lbox1onchange(e)
{
var app = UiApp.getActiveApplication();
// var value = e.parameter.lbox1;
var lbox1value = e.parameter.ListBox1;
var lbox2 = app.getElementById('ListBox2');
var lbox2_Item = SitesApp.getSite('phichdaica').getChildByName('manga').getChildByName(lbox1value).getChildren();
for(var i=lbox2_Item.length-1; i >= 0; i--)
{
lbox2.addItem(lbox2_Item[i].getTitle());
}
return app;
}

I don't think it will speed up the process but you could use just one handler function to do that : on change listBox1, clear listBox 2 and re-populate it immediately. What is taking some time is the call to site's content so the difference might not be significative but the 'logic' of your script would be improved ;-)
Looking at your page, I see that listBox 2 is never cleared... is that a temporary issue ? Did you change something recently ?
Also, what is supposed to happen when something is selected in listBox2 ?
EDIT : following your comment, if you want to improve user experience concerning the 'responsiveness' of your UI the best way is to use client handlers to trigger the visibility of a 'wait message' for example(something like "Updating the list"). I usually use an animated gif that I made visible with a client handler and that is made invisible again when the server handler returns (ie I set it invisible in the server handler function).
here is a working example, just try to change the date in the upper right corner.

Related

Google Apps Script Lockservice Not Working

I am calling a sidebar in a Google Sheets bound script. I am trying to prevent multiple users from opening the sidebar at a time. The code below is how I am attempting to achieve this:
function loadM1DocsSideBar() {
var lock = LockService.getScriptLock();
lock.tryLock(0);
if (!lock.hasLock()) {
SpreadsheetApp.getUi().alert("WARNING! Function currently in use by another user. Please try again later.");
return;
}
Logger.log(lock);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName("Metadata");
var dataRange = sh.getRange("metadataRecord").clearContent();
var form = HtmlService.createHtmlOutputFromFile('M1DocsConfiguratorSidebar').setTitle('Verification Project Library Creator');
SpreadsheetApp.getUi().showSidebar(form);
lock.releaseLock();
}
During testing both first and second users can launch the sidebar at the same time. Can anyone enlighten me where I am going wrong.
Issue:
The script becomes inaccessible only during the brief time the first execution hasn't finished. After that, other users can execute this, even if the sidebar is opened by user #1: the script has already ended execution. The fact that a certain user has the sidebar opened is not registered by your script.
Workaround:
A possible workaround would be using Properties Service to set and retrieve information about whether the sidebar is open, instead of using LockService. The idea would be the following:
When your main function starts, check whether there's a script property SIDEBAR_OPEN equal to true (see getProperty). If that's the case, show your alert and stop the execution. This would be parallel to your current tryLock and hasLock sections.
If the script property is not present, or not equal to true, that means no-one else has opened the sidebar. Your script can now open the sidebar, and set the script property SIDEBAR_OPEN to true (see setProperty.
On your sidebar, have a button that will close the sidebar, and which will as well call a function (setClosedSidebar in the sample below) that will set SIDEBAR_OPEN to false.
Code sample:
function loadM1DocsSideBar() {
var sidebarOpen = isSidebarOpen();
if (sidebarOpen) {
SpreadsheetApp.getUi().alert("WARNING! Function currently in use by another user. Please try again later.");
return;
}
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName("Metadata");
var dataRange = sh.getRange("metadataRecord").clearContent();
var form = HtmlService.createHtmlOutputFromFile('M1DocsConfiguratorSidebar').setTitle('Verification Project Library Creator');
setOpenedSidebar();
SpreadsheetApp.getUi().showSidebar(form);
}
function setOpenedSidebar() {
var props = PropertiesService.getScriptProperties();
props.setProperty("SIDEBAR_OPEN", "true");
}
function setClosedSidebar() { // Call me when closing the sidebar
var props = PropertiesService.getScriptProperties();
props.setProperty("SIDEBAR_OPEN", "false");
}
function isSidebarOpen() {
var props = PropertiesService.getScriptProperties();
return JSON.parse(props.getProperty("SIDEBAR_OPEN"));
}
Note:
As a downside to this workaround, this will only work if the sidebar is closed through the button, not by clicking the closing icon nor by refreshing the tab. You should probably set a timeout anyway via Utilities.sleep, so that the sidebar becomes accessible to other users after some time. Be careful with this.
Alternatively, you could use Utilities.sleep to keep the script running for some time after displaying the sidebar, so that other users cannot open the sidebar right after the first user did that.

Using server handlers with modal dialogs

I am displaying a User Interface over a sheet using showModalDialog passing in the app I just created. I also setup a button with a server handler. When server handler function is called I try to get the app again using "UiApp.getActiveApplication()" to hide some elements and show some different elements, however, the changes are not reflected. At the end of the method I tried to close the app, and show a new modal dialog, I tried to return the app, I tried to do nothing, and nothing seems to work.
I can't post my whole code since it is very long, so I made a very simple version that gets the point across. When I put some logging statements in testHandler() it proves that the code is running.
function test() {
var app = UiApp.createApplication().setHeight(700).setWidth(1500);
var label = app.createLabel("Hi").setId("label");
var label2 = app.createLabel("GoodBye").setId("label2").setVisible(false);
var button = app.createButton("Press Me").setId("button");
app.add(label);
app.add(label2);
app.add(button);
var testHandler = app.createServerHandler('testHandler');
testHandler.addCallbackElement(label);
testHandler.addCallbackElement(label2);
button.addClickHandler(testHandler);
SpreadsheetApp.getUi().showModalDialog(app, 'Test');
}
function testHandler() {
var app = UiApp.getActiveApplication();
app.getElementById('label').setVisible(false);
app.getElementById('label2').setVisible(true);
// Not sure what to do now
}
Thank you in advance for your help
return app; //where you are not sure what do do

Multiple Page UI using UiService

I would like to use Google Apps Script UiService to produce a multiple page user interface.
Here's what I've got so far:
function doGet(e)
{
var app=UiApp.createApplication();
var nameLabel=app.createLabel('Name:');
var button=app.createButton("next");//my button on clicking,trying to divert to other UI
var handler=app.createServerHandler("myclick");
button.addClickHandler(handler);
app.add(namelabel);
app.add(button);
return app;
}
function myClick(){
//on clicking the button it should call the other ui or other html page
is there any method for that.}
How can I do this?
You should look at How To Allow Users to Review Answers before Submiting Form?, which has an example that does this.
The idea is to create your UiApp with multiple Panels, then show or hide them in response to user actions, using setVisible(). (If you were using the HtmlService, you would enclose your "pages" in different <div>s, and change their display attributes. See toggle show/hide div with button?.)
The Best Practices also describes use of client-side handlers for responsiveness, so let's try that.
/**
* Very simple multiple page UiApp.
*
* This function defines two panels, which appear to the end user
* as separate web pages. Visibility of each panel is set to
* control what the user sees.
*/
function doGet() {
var app = UiApp.createApplication();
var page1 = app.createFlowPanel().setId('page1');
var page2 = app.createFlowPanel().setId('page2');
// Content for Page 1
page1.add(app.createLabel('Page 1'));
var page1Button = app.createButton('Next Page');
page1.add(page1Button);
// Create client handler to "change pages" in browser
var gotoPage2 = app.createClientHandler()
.forTargets(page1).setVisible(false)
.forTargets(page2).setVisible(true);
page1Button.addClickHandler(gotoPage2);
// Content for Page 2
page2.add(app.createLabel('Page 2'));
var page2Button = app.createButton('Previous Page');
page2.add(page2Button);
// Create client handler to "change pages" in browser
var gotoPage1 = app.createClientHandler()
.forTargets(page1).setVisible(true)
.forTargets(page2).setVisible(false);
page2Button.addClickHandler(gotoPage1);
app.add(page1);
app.add(page2);
// Set initial visibility
page1.setVisible(true);
page2.setVisible(false);
return app;
}
That works for changing the view of the UI. To extend this for general purposes, you would likely want to add server-side handlers to the same buttons to perform work, and update the contents of the panels as things progress.
Here is working code
that demonstrates a multiple page form, i.e. it does the initial doGet() and then lets you advance back and forth doing multiple doPost()'s. All this is done in a single getForm() function called by both the standard doGet() and the doPost() functions.
// Muliple page form using Google Apps Script
function doGet(eventInfo) {return GUI(eventInfo)};
function doPost(eventInfo) {return GUI(eventInfo)};
function GUI (eventInfo) {
var n = (eventInfo.parameter.state == void(0) ? 0 : parseInt(eventInfo.parameter.state));
var ui = ((n == 0)? UiApp.createApplication() : UiApp.getActiveApplication());
var Form;
switch(n){
case 0: {
Form = getForm(eventInfo,n); // Use identical forms for demo purpose only
} break;
case 1: {
Form = getForm(eventInfo,n); // In reality, each form would differ but...
} break;
default: {
Form = getForm(eventInfo,n) // each form must abide by (implement) the hidden state variable
} break;
}
return ui.add(Form);
};
function getForm(eventInfo,n) {
var ui = UiApp.getActiveApplication();
// Increment the ID stored in a hidden text-box
var state = ui.createTextBox().setId('state').setName('state').setValue(1+n).setVisible(true).setEnabled(false);
var H1 = ui.createHTML("<H1>Form "+n+"</H1>");
var H2 = ui.createHTML(
"<h2>"+(eventInfo.parameter.formId==void(0)?"":"Created by submission of form "+eventInfo.parameter.formId)+"</h2>");
// Add three submit buttons to go forward, backward and to validate the form
var Next = ui.createSubmitButton("Next").setEnabled(true).setVisible(true);
var Back = ui.createSubmitButton("Back").setEnabled(n>1).setVisible(true);
var Validate = ui.createSubmitButton("Validate").setEnabled(n>0).setVisible(true);
var Buttons = ui.createHorizontalPanel().add(Back).add(Validate).add(Next);
var Body = ui.createVerticalPanel().add(H1).add(H2).add(state).add(Buttons).add(getParameters(eventInfo));
var Form = ui.createFormPanel().setId((n>0?'doPost[':'doGet[')+n+']').add(Body);
// Add client handlers using setText() to adjust state prior to form submission
// NB: Use of the .setValue(val) and .setValue(val,bool) methods give runtime errors!
var onClickValidateHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)));
var onClickBackHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)-1));
Validate.addClickHandler(onClickValidateHandler);
Back.addClickHandler(onClickBackHandler);
// Add a client handler executed prior to form submission
var onFormSubmit = ui.createClientHandler()
.forTargets(state).setEnabled(true) // Enable so value gets included in post parameters
.forTargets(Body).setStyleAttribute("backgroundColor","#EEE");
Form.addSubmitHandler(onFormSubmit);
return Form;
}
function getParameters(eventInfo) {
var ui = UiApp.getActiveApplication();
var panel = ui.createVerticalPanel().add(ui.createLabel("Parameters: "));
for( p in eventInfo.parameter)
panel.add(ui.createLabel(" - " + p + " = " + eventInfo.parameter[p]));
return panel;
}
The code uses a single "hidden" state (here visualized in a TextBox) and multiple SubmitButton's to allow the user to advance forward and backward through the form sequence, as well as to validate the contents of the form. The two extra SubmitButton's are "rewired" using ClientHandler's that simply modify the hidden state prior to form submission.
Notes
Note the use of the .setText(value) method in the client handler's. Using the Chrome browser I get weird runtime errors if I switch to either of the TextBox's .setValue(value) or .setValue(value, fireEvents) methods.
I tried (unsuccessfully) to implement this logic using a Script Property instead of the hidden TextBox. Instead of client handlers, this requires using server handlers. The behavior is erratic, suggesting to me that the asynchronous server-side events are occurring after the form submission event.
You could load different UI's on reading the parameters in your app.
The doGet(e) passes the parameters in the app's url. This way you could call your app with for example: ?myapp=1 (url parameter).
in your doGet you could read that parameter with: e.parameter.myapp
This way you could load different applications depending on the parameters that where passed.
You could just change your button with a link (to your own app, with different url parameters).
You could also do it with buttons and handlers but the above way has my preference.
If you want to use a button<>handler just change you main (first panel) and each time add a completely new panel to your app object. This way you would start from scratch (i.e. create a new application).

Need a way to parametize an array sort function, with parameters set in a handler

I'm writing a web app that displays a subset of rows from a spreadsheet worksheet. For the convenience of users, the data is presented in a grid, each row selected from the spreadsheet forms a row in the grid. The number of rows relevant to each user grows over time.
The header of the grid is a set of buttons, which allow the user to control the sort order of the data. So if the user clicks the button in the header of the first column, the array that populates the grid is sorted by the first column and so forth. If you click the same column button twice, the sort order reverses.
I used script properties to communicate the selected sort field and order between the handler responding to the button presses, and the order function called by the sort.
So in doGet():
// Sort Field and Order
ScriptProperties.setProperties({"sortField": "date","sortOrder": "asc"});
And in the button handler:
function sortHandler(event) {
var id = event.parameter.source;
if (id === ScriptProperties.getProperty("sortField")) {
ScriptProperties.setProperty("sortOrder", (ScriptProperties.getProperty("sortOrder") === "asc")?"desc":"asc");
} else {
ScriptProperties.setProperties({"sortField": id, "sortOrder": "asc"});
}
var app = UiApp.getActiveApplication();
app.remove(app.getElementById("ScrollPanel"));
createForm_(app);
return app;
}
And the order function itself (this is invoked by a sort method on the array: array.sort(order); in the code that defines the grid):
function orderFunction(a, b) {
var sortParameter = ScriptProperties.getProperties();
var asc = sortParameter.sortOrder === "asc";
switch(sortParameter.sortField) {
case "date":
var aDate = new Date(a.date);
var bDate = new Date(b.date);
return (asc)?(aDate - bDate):(bDate - aDate);
case "serviceno":
return (asc)?(a.serviceno-b.serviceno):(b.serviceno-a.serviceno);
default: // lexical
var aLex = String(a[sortParameter.sortField]).toLowerCase();
var bLex = String(b[sortParameter.sortField]).toLowerCase();
if (aLex < bLex) {
return (asc)?-1:1;
} else if (aLex > bLex) {
return (asc)?1:-1;
} else {
return 0;
}
}
}
The fly in the ointment with this design is Google. Once the array gets to a certain size, the sort fails with an error that the Properties service is being called too frequently. The message suggests inserting a 1s delay using Utilities.sleep(), but the grid already takes a long time to render already - how is it going to go if the array takes 1s to decide the order of two values?
I tried reimplementing with ScriptDB, but that suffers the same problem, calls to ScriptDB service are made too frequently for Google's liking.
So how else can I implement this, without the orderFunction accessing any App Script services?
If you are looking for temporary storage, I prefer CacheService to ScriptProperties.
Use
CacheService.getPrivateCache().put and
CacheService.getPrivateCache().get
You could also use a hidden widget to pass information between the doGet an handler functions, it will also be much faster than calling the scriptProperties service (have a look at the execution transcript to see how long it takes, you'll be surprised)
On the other hand if you really want to keep using it you can also try to (slightly) reduce the number of calls to the scriptProperties service, for example in your sortHandler(event) function :
function sortHandler(event) {
var id = event.parameter.source;
var sortField = ScriptProperties.getProperty("sortField");
var sortOrder = ScriptProperties.getProperty("sortOrder");
if (id === sortField) {
ScriptProperties.setProperty("sortOrder", (sortOrder === "asc")?"desc":"asc");
} else {
ScriptProperties.setProperties({"sortField": id, "sortOrder": "asc"});
}
var app = UiApp.getActiveApplication();
app.remove(app.getElementById("ScrollPanel"));
createForm_(app);
return app;
}
...how else can I implement this?
Use HTMLService with jQuery DataTable which does all the ordering that you can imagine and a whole lot more without scripting button press logic and the like. There is an example of the spreadsheet to basic table here http://davethinkingaloud.blogspot.co.nz/2013/03/jsonp-and-google-apps-script.html

Replicate tab Panel in GUI Builder google apps

I think I've seen this answer, but I can't remember where for certain.
I'm trying to to create a tabbed panel interface using the GUI Builder, but don't see that option. The part I seem to recall is someone having an approach to replicate that in the GUI Builder. I just can't seem to find that information in my brain, the old google groups or here.
Can someone jog my memory?
Thank you...
Maybe the post you were referring to was this one ? Anyway, no matter how much panels you have, yo could design them in the GUI, one on top of the other or (more simply) one under each other in a 'parent' vertical panel and play with client handlers to show/hide the one you need.
I have an example here with 2 panels in an UI, the UI is designed with script but that is not important, look at the client handlers to see how it works.
If I have some free time tonight I'll make a demo script for 4 panels in GUI ;-).
EDIT : here is a test example (standalone) or embedded in a Google site + link to the script (make a copy to edit)
Note that in the GUI builder you'l have to 'play' with visibility of each panel to work on it, I used a main panel large enough to hold 2 panels together so you can have a better vision of "harmony" between panels (which is not the case in my test;-))
and the code (very simple basic example 4 panels with each of them a textBox & a Label, just to test the handlers on the buttons):
function doGet() {
var app = UiApp.createApplication();
var UI=app.loadComponent('multiUi')
var panel1 = app.getElementById('panel1')
var panel2 = app.getElementById('panel2')
var panel3 = app.getElementById('panel3')
var panel4 = app.getElementById('panel4')
var Button1 = app.getElementById('Button1')
var Button2 = app.getElementById('Button2')
var Button3 = app.getElementById('Button3')
var Button4 = app.getElementById('Button4')
var pHandler1 = app.createClientHandler()
.forTargets(panel1).setVisible(true).forTargets(panel2,panel3,panel4).setVisible(false)
Button1.addClickHandler(pHandler1)
var pHandler2 = app.createClientHandler()
.forTargets(panel2).setVisible(true).forTargets(panel1,panel3,panel4).setVisible(false)
Button2.addClickHandler(pHandler2)
var pHandler3 = app.createClientHandler()
.forTargets(panel3).setVisible(true).forTargets(panel2,panel1,panel4).setVisible(false)
Button3.addClickHandler(pHandler3)
var pHandler4 = app.createClientHandler()
.forTargets(panel4).setVisible(true).forTargets(panel2,panel3,panel1).setVisible(false)
Button4.addClickHandler(pHandler4)
app.add(UI)
return app;
}
The following code makes the tabs, based on the array you put in, dynamically:
function doGet() {
// create application
var app = UiApp.createApplication();
// set array
var aTabs = ['donald','katrijn','dagobert'];
// create tab panel
var pTab = app.createTabPanel();
// add tabs to panel
for(var k=0; k<pTabs.length; k++) {
pTab.add(app.createLabel("This tab is reserved for " + aTabs[k]), aTabs[k]);
}
// add panel to application
app.add(pTab);
// set focus to first tab
pTab.selectTab(0);
// return to application
return app;
}
See link for tabPanel reference.
Publishing your script as a web-app, allows you to insert the script on a google sites.