refreshing a button's text is not working with GAS - google-apps-script

I have the following code which creates a simple app to allow the user to enter two values and click a button:
function start() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var app = UiApp.createApplication();
app.setTitle("Appraisals Analysis");
app.setHeight(100);
app.setWidth(500);
var grid = app.createGrid(3, 2);
grid.setId("grid");
grid.setCellSpacing(2);
grid.setCellPadding(2);
var uafLabel = app.createLabel("Unprocessed apparaisals folder name: ");
uafLabel.setStyleAttributes({"font-weight": "bold"});
var uafTextBox = app.createTextBox();
uafTextBox.setName('uafTextBox').setId('uafTextBox');
uafTextBox.setText('Unprocessed Appraisals');
grid.setWidget(0, 0, uafLabel);
grid.setWidget(0, 1, uafTextBox);
var pafLabel = app.createLabel("Processed apparaisals folder name: ");
pafLabel.setStyleAttributes({"font-weight": "bold"});
var pafTextBox = app.createTextBox();
pafTextBox.setName('pafTextBox').setId('pafTextBox');
pafTextBox.setText('Processed Appraisals');
grid.setWidget(1, 0, pafLabel);
grid.setWidget(1, 1, pafTextBox);
var button = app.createButton('Submit').setId("submitButton");
grid.setWidget(2, 0, button);
var mypanel = app.createVerticalPanel();
mypanel.add(grid);
app.add(mypanel);
var clickHandler = app.createServerClickHandler("parseFiles");
button.addClickHandler(clickHandler);
clickHandler.addCallbackElement(grid);
ss.show(app);
}
I then have the parseFiles function which can take up to 2 minutes to do its job as follow
function parseFiles(e) {
var app = UiApp.getActiveApplication();
var processedFolder = DocsList.getFolder(e.parameter.pafTextBox);
var workingFolder = DocsList.getFolder(e.parameter.uafTextBox);
var appraisals = workingFolder.find('Performance Appraisal');
app.getElementById("submitButton").setText("Parsing Files...");
for (var i in appraisals) {
var doc = DocumentApp.openById(appraisals[i].getId());
parseDocument(doc, getEmpName(doc.getName()));
}
return app;
}
My problem is that when I click the button, the work gets done, but the button text stays as "Submit" instead of changing to "Parsing Files...". Once the work is over, then the button changes.
Any idea what I may be doing wrong?
Regards
Crouz

You got nothing wrong, just the concept. Think like this: all Apps Script code you write runs on a google server (server-side), but the interface is (obviously) shown on your computer (client-side). The Apps Script "environment" has a client-side script (that we do not have access or control of) that receives the information on how to build the interface you defined in your code (server-side).
So, everything you do in your code gets updated at once, in a bundle, after your function finishes. And that's why we need to return app, so that our UiApp definition gets sent/returned to the client-side that have triggered the script.
For very simple situations, like disabling or setting the text on a button or label, there's a clientHandler that can perform basic operations on directly the client-side without requiring a network trip to the server-side to run your custom code. Since these operations are done on the client-side they're done "instantly". Note that this is not for generic code, but only predefined operations. clientHandlers are really meant just for simple stuff. It's difficult (if not impossible) to do complex operations.
Here's my suggestion using a clientHandler:
function start() {
//your current code...
clickHandler.addCallbackElement(grid);
var clientHandler = app.createClientHandler().forEventSource().setText('Parsing Files...').setEnabled(false);
button.addClickHandler(clientHandler);
ss.show(app);
}
function parseFiles(e) {
//...
app.getElementById("submitButton").setText("Submit again").setEnabled(true);
//...
return app;
}
Note that you can add multiple handlers, client or server, to a button (or any other widget that accept handlers) and all of them will run concurrently.
Also, it's very important to notice that we're talking about UiApp here, when using HtmlService the approach is significantly different.

Related

Google App Script UiApp when button pressed change it's color immediately before processing script?

I want script when button is pressed change its color immediately but actually it's change it's color after script of copyProject is completed, copyProject function takes around 40 second
I have thought to split handler function to make it change color on press and before running copyProject Function, any ideas ?
function copyProject(e)
{
var app=UiApp.getActiveApplication();
var button=app.getElementById("button");
process(app,button);
var sourceProjectID=e.parameter.id;
copProject(sourceProjectID);
return app.close();
}
function process(app,button)
{
button.setStyleAttribute("color", "red").setText("Processing");
return app;
}
Thanks
Here is an example of a Client Server and a serverHandler on a button that simulates the behavior you want : the button changes color when you click it ... after the long serverHandler function it shows a message.
function doGet(){
var app = UiApp.createApplication();
var handler = app.createServerHandler('test');
var cHandler = app.createClientHandler();
var btn = app.createButton('click this button').addClickHandler(handler).addClickHandler(cHandler);
cHandler.forEventSource().setStyleAttributes({'color':'red'})
app.add(btn);
return app
}
function test(e){
var app = UiApp.getActiveApplication();
app.add(app.createLabel('operation completed'));
Utilities.sleep(4000);
return app;
}
Live test here
Processing happens on the server, thus it will only redraw the app after the process is complete and your UI will not be responsive.
The proper way to work with the UI and make it responsive is to use client handler which enable you update your UI on the fly. Also look into validators, which are useful to validate inputs before you process them without going to the server.
https://developers.google.com/apps-script/uiapp#ClientHandlers
https://developers.google.com/apps-script/uiapp#Validators
Let me know if I can help you further

Removing Elements in a StackPanel

All,
I am trying to implement a StackPanel containing multiple sub-panels to collect user data. Each subpanel (I have named them SwapRecordPanels) represents a line in a Google spreadsheet containing the data for two people who want to swap which days they will work. The SwapRecordPanels contain a delete button. On clicking the delete button, I would like the SwapRecordPanel to remove itself from the StackPanel.
However, I have found that when I attempt to remove an element from a StackPanel, the change does not register in the GUI. As an attempt at the proof of the concept that I could delete elements in a StackPanel, I added an extra button titled "Click Me". As you can see in the code below, its handler attempts to remove an element from the StackPanel "LeftPanel." However, when I click the button, the "LeftPanel" remains unchanged. I'm wondering if there's some sort of refresh/repaint method to be called to update the GUI. If not, could anyone offer any advice? Any help would be greatly appreciated.
function myViewer() {
var myapp = doGet();
SpreadsheetApp.getActive().show(myapp)
}
function doGet() {
var myapp = UiApp.createApplication();
var LeftPanel = myapp.createStackPanel().setId("LeftPanel");
var LeftScrollPanel = myapp.createScrollPanel(LeftPanel).setPixelSize(410,200)
myapp.add(LeftScrollPanel)
var SwapPane1 = SwapRecordPanel(LeftPanel);
var SwapPane2 = SwapRecordPanel(LeftPanel);
LeftPanel.add(SwapPane1,"Test 1 Header");
LeftPanel.add(SwapPane2, "Test 2 Header");
var myButton = myapp.createButton("Click Me").addClickHandler(myapp.createServerHandler("ButtonClicker"));
myapp.add(myButton);
return myapp;
}
function ButtonClicker()
{
Logger.log("I was clicked")
var myapp = UiApp.getActiveApplication();
var LeftPanel = myapp.getElementById("LeftPanel");
LeftPanel.remove(myapp.getElementById("panel1"));
}
You have to "return" the changes to the app from the handler function for the UI to update.
Simply add return myapp; at the end of the ButtonClicker() function.

Can I develop an wizard application (sequentially submit forms)?

In my understanding now, only one doGet() can trigger unique doPost() in a Google Apps Script application.
I would like to perform a Software Publisher System that user upload the file or fill up revision information in forms and push submit to the next step. The final page will show the input information, send email to guys and complete all operation.
But how do I enter next form after the submit button pushed?
I have tried a method that creating the 2nd step and 3rd step forms in the doPost(), and using try...catch to difference which step form triggered the current step, like the following code.
(Because any steps can't get the callback item throw by non-previous step, then it arises an exception)
It works very well but I think it doesn't make sens and very silly. Have any better solutions? Thanks, please.
//---------------------------------------------------------------------------
function doGet(e)
{
var app = UiApp.createApplication().setTitle("AP Publisher");
createFileUploadForm(app);
return app;
}
//---------------------------------------------------------------------------
function doPost(e)
{
var app = UiApp.getActiveApplication();
try {
// 2nd step form
var fileBlob = e.parameter.thefile;
createRevisionForm();
}
catch(error) {
try {
// 3rd step form
createConfirmForm(e);
}
catch(error2) {
//Complete
sendMail(e);
modifySitePageContent(e);
saveHistoryFile(e);
showConfirmedInfo(e);
}
}
return app;
}
This answer is copied entirely from create a new page in a form dynamically, based on data of the prev. page.
Using the UiApp service, you have one doGet() and one doPost() function... but here's a way to extend them to support a dynamic multi-part form. (The example code is borrowed from this answer.)
Your doGet() simply builds part1 of your form. In the form, however, you need to identify your form by name, like this:
var form = app.createFormPanel().setId("emailCopyForm");
You doPost() then, will pass off handling of the post operation to different functions, depending on which form has been submitted. See below. (Also included: reportFormParameters (), a default handler that will display all data collected by a form part.)
/**
* doPost function with multi-form handling. Individual form handlers must
* return UiApp instances.
*/
function doPost(eventInfo) {
var app;
Logger.log("Form ID = %s", eventInfo.parameter.formId);
// Call appropriate handler for the posted form
switch (eventInfo.parameter.formId) {
case 'emailCopyForm':
app = postEmailCopyForm(eventInfo);
break;
default:
app = reportFormParameters (eventInfo);
break;
}
return app;
}
/**
* Debug function - returns a UiInstance containing all parameters from the
* provided form Event.
*
* Example of use:
* <pre>
* function doPost(eventInfo) {
* return reportFormParameters(eventInfo);
* }
* </pre>
*
* #param {Event} eventInfo Event from UiApp Form submission
*
* #return {UiInstance}
*/
function reportFormParameters (eventInfo) {
var app = UiApp.getActiveApplication();
var panel = app.createVerticalPanel();
panel.add(app.createLabel("Form submitted"));
for (var param in eventInfo.parameter) {
switch (param) {
// Skip the noise; these keys are used internally by UiApp
case 'lib':
case 'appId':
case 'formId':
case 'token':
case 'csid':
case 'mid':
break;
// Report parameters named in form
default:
panel.add(app.createLabel(" - " + param + " = " + eventInfo.parameter[param]));
break;
}
}
app.add(panel);
return app;
}
To generate each form part, subsequent form handlers can use the data retrieved in previous parts to dynamically add new Form objects to the ui.
I think it would be simpler to use 3 (or more) different panels in your doGet function with all the items you need and to play with their visibility.
At first only the 1rst panel would be visible and, depending on user input (using client Handlers to handle that) show the next ones (and eventually hide the first one).
In the end the submit button will call the doPost and get all data from the doGet.
First a tip of my hat to Mogsdad. His post(s) were guiding lights in the darkly documented path that led me here. Here is some 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.

Google Apps Script, HTML addClickHandler ServerHandler does NOT work

Can anyone confirm that HTML widgets accept ClickHandlers on the Server side ? I can't get my below code to work.
I create a serverHandler (and for good measure I have even added a useless callback element). Subsequently, I add it to a HTML.addClickHander (for good measure I have even added it to .addMouseUpHandler as well). The function is NOT executed.
var mouseclick = app.createServerHandler("handleTrainingClick_").addCallbackElement(lstFilter);
var params = [ "fromOrg", "trainingTitle", "dueDate", "medical", "status" ];
var resultSet = blSelectActiveTrainings_();
while (resultSet.hasNext()) {
var training = resultSet.next();
var html = TRAINING_ROW;
for (var pI in params) {
html = html.replace("$"+params[pI], training[params[pI]]);
}
pnlList.add(app.createHTML(html).setId(training.id).addClickHandler(mouseclick).addMouseUpHandler(mouseclick)
.addMouseMoveHandler(mousemove).addMouseOutHandler(mouseout).addMouseOverHandler(mouseover));
}
function handleTrainingClick_(e) {
Logger.log(e.source);
var app = UiApp.getActiveApplication();
return app;
}
HTML widgets server side handlers work just fine. It was an incorrect reference in my code. Thanks all.

What is the best way to manage two UI's?

I have created two user interfaces. How can I close the first one and activate the next? Is it possible to have two UI under Google apps script?
I have try something like:
var app = UiApp.getActiveApplication();
app.add(app.loadComponent("APPGui"));
var panel1 = app.getElementById("LoginPanel1");
panel1.setVisible(false);
return app;
The easiest way is probably to design both panels in the same GUI builder, one over each other in 2 separate panels, the 'login panel' being above the other it will mask the other one when active. As you set it 'invisible', you'll see the one underneath.
Depending on your use case the login panel might hide all or only a part of your main panel.
The GUI builder has all the necessary tools to decide which is in front or backwards.
Here's and example of three dialogs shown one after the other, maintaining state/data between them via the CacheService object.
(You could use UserProperties, ScriptProperties or even a Hidden Field as an alternative, each has their own scope though...)
Hopefully this makes sense without explaining what each dialog in the UI Builder contains.
function showDialog1(){
var app = UiApp.createApplication();
app.add( app.loadComponent("Dialog1") );
SpreadsheetApp.getActiveSpreadsheet().show(app);
}
function onDialog1OKButton(e){
CacheService.getPrivateCache().put("n1", e.parameter.n1);
var app = UiApp.getActiveApplication();
var d2 = app.loadComponent("Dialog2");
app.add(d2);
SpreadsheetApp.getActiveSpreadsheet().show(app);
}
function onDialog2OKButton(e){
var c = CacheService.getPrivateCache();
c.put("n2", e.parameter.n2);
var app = UiApp.getActiveApplication();
app.add(app.loadComponent("DialogResult"));
var n1 = c.get("n1");
var n2 = c.get("n2");
var l = app.getElementById("Label2");
l.setText( "" + n1 + " + " + n2 + " = " + (parseInt(n1) + parseInt(n2)) );
SpreadsheetApp.getActiveSpreadsheet().show(app);
}
I prefer to build multiple GUI. With this code you can jump between them.
function doGet() {
var app = UiApp.createApplication();
var base0 =app.createAbsolutePanel().setId('GUI_base0').setHeight('630px').setWidth('1125px');
app.createAbsolutePanel().setId('GUI_base1'); // create all abs_panells but not use
// you need to create all abspanels if you want to jump between them
app.createAbsolutePanel().setId('GUI_base2'); // create here all the absolute panels (1 for every GUI)
// app.createAbsolutePanel() ... GUI3, GUI4 ...
var component0 = app.loadComponent("GUI_password"); // load first GUI (his name is "password"
/// this is an example of code for the 1st GUI ////////////////////
/// I can check if the user can see the second GUI
var label_ID = app.getElementById('LB_ID');
var user = Session.getActiveUser().getEmail();
if ( user == 'XXX#yyyy.com' ) {
label_ID.setText(user).setTag(user); // only show if ....
}
////////////////////////////////////////////////////////////////////
base0.add(component0); // GUI_password over absolute panel
app.add(base0);
// handler Button1 // we can show a button only if the password is correct or is a valid user or ...
app.getElementById('BT_jump').addClickHandler(app.createServerHandler('NOW_gui1'));
return app;
};
function NOW_gui1(e) {
var app = UiApp.getActiveApplication();
var base0 = app.getElementById("GUI_base0").setVisible(false); // hide 1st abs_panel created with code
var base2 = app.getElementById("GUI_base2").setVisible(false); // hide 3rd abs_panel created with code
/// hide all others abs_panel
var base1 = app.createAbsolutePanel().setId('GUI_base1').setHeight('630px').setWidth('1125px'); // maybe get by ID ??, but this work
var component1 = app.loadComponent("GUI_1"); // load the second GUI
base1.add(component1); // load GUI_1 over 2n absolute panel
app.add(base1);
// HERE THE CODE OF THE GUI_1
// handler Button2
app.getElementById('BT_jump_1_to_2').addClickHandler(app.createServerHandler('NOW_gui2'));
return app;
};