Can a Google Spreadsheet Apps Script library contain a user dialog? - google-apps-script

Are there restrictions on what may be in an Apps Script library to be used by Google Spreadsheets? Specifically, can a library include an HTML dialog?
I've created a spreadsheet script that adds a menu item to present the user with a dialog box. It uses
HtmlService.createHtmlOutputFromFile('mappingForm').setSandboxMode(HtmlService.SandboxMode.IFRAME)
as described in https://developers.google.com/apps-script/guides/html/communication. The HTML file includes HTML, CSS and JavaScript with jQuery. It uses google.script.run to populate the dialog with data from the spreadsheet and to submit a form to it.
This all works fine in the original spreadsheet.
I need multiple spreadsheets to use this same code, however, so I'm trying to follow the general idea of Google Spreadsheet Scripts shared across spreadsheets (not libraries) to have a master script with a spreadsheet template and multiple copies.
I followed the directions at https://developers.google.com/apps-script/guide_libraries to create a library from the original spreadsheet. When another spreadsheet uses the library, I'm able to get the dialog to appear, but all calls back to the server (either to populate the dialog or to submit a form) fail with an error caught browser-side by the google.script.run.withFailureHandler as an Error object with properties:
message: "We're sorry, a server error occurred. Please wait a bit and try again."
name: "ScriptError"
I've placed Logger calls in the apps script to see if the server-side functions are being called but none of them are being hit. The script editor's Execution Transcript shows:
[14-12-27 19:38:05:340 PST] Starting execution
[14-12-27 19:38:05:372 PST] Execution failed: We're sorry, a server error occurred. Please wait a bit and try again. [0.0 seconds total runtime]
The client is making the call, but something is failing before it reaches the spreadsheet script.
This makes me wonder whether
I need to do something differently for the code to work as a library.
Libraries can't have dialogs.
There's a server bug.
Thanks in advance for any suggestions.

I was able to have a working library containing an HTML dialog by doing the following.
Move the script and HTML files from the original spreadsheet to a standalone script project. Take note of the library's Project key in the Info tab of File > Project properties.... It will be needed by any spreadsheet that intends to use the library.
If the standalone script project is to used by others, click its Share button to make it shareable to anyone with a link, otherwise it will silently fail for them.
If the HTML dialog needs to call back to a library function (to get or submit data), the library function must be present in the spreadsheet that uses the library, or you'll get an error message in the browser's JavaScript console.
In the spreadsheet that uses the library: Tools > Script editor... Click Resources > Libraries.... In the "Included Libraries" dialog box, enter the standalone project's key in the Find a Library text box, click Select, then choose the appropriate Version, change the Identifier, if necessary, and Save. The Identifier value creates an object of the same name for use by the spreadsheet's script to call library functions. In my case, it is SignupFormResponsesSheet.
In the same Script Editor's code editor, add wrapper functions that call library functions, including any that will be called back from the HTML dialog. My library has an onOpen() which creates two menu items to show HTML dialogs, so I added
function onOpen() {
SignupFormResponsesSheet.onOpen();
}
function showMappingForm() {
SignupFormResponsesSheet.showMappingForm();
}
function showSubmitForm() {
SignupFormResponsesSheet.showSubmitForm();
}
My HTML dialog has a number of callbacks to get and submit data, so rather than writing a wrapper function for each, I added one function to cover all of them by taking advantage of the way Apps Script treats a library as an object containing functions. The first argument is a string naming the library function to call. Any additional arguments are passed to the library function.
function runSignupFormResponseFunction(funcName, varargs) {
return SignupFormResponsesSheet[funcName].apply(this,
Array.prototype.slice.call(arguments, 1));
}
Because of the restriction identified in step 3 above, the JavaScript in the HTML dialog uses google.script.run to call the runSignupFormResponseFunction whenever it needs to get or submit data. For example, it has two lists that are dynamically populated with server data from the library's getRangeLabels and getColumnExamples functions (and one must be populated before the other), so the code is
google.script.run
.withFailureHandler(showError)
.withSuccessHandler(function(ranges) {
loadRanges(ranges);
// once ranges are loaded, load columns
google.script.run
.withSuccessHandler(loadColumns)
.withFailureHandler(showError)
.runSignupFormResponseFunction("getColumnExamples");
})
.runSignupFormResponseFunction("getRangeLabels");
This worked for me today. I hope it works for others who may find this question.

Related

Centralized script for multiple spreadsheets

I have created a Notification script and would like to use it for multiple spreadsheets (produced by Forms). I would like to have a centralized script and referencing it on my multiple spreadsheets in order to simplify maintenance.
I have tried to use the SpreadsheetApp.openById() like below but looks like this feature has been disabled for security reasons and is not supported anymore.
function append()
{
SpreadsheetApp.openById("1xdePF..........................");
}
Any idea on how to use the same script for multiple spreadsheets?
The documentation specifies that openById opens the spreadsheet with the given ID.
You cannot use this method to open a script.
To open an Apps Script, go to https://script.google.com/home/my and clock on the script of your choice.
For bound scripts, you open the document to which the script is bound and go to Tools->Script Editor
Preface
This answer is supplementary to ziganotschka's, since you indeed cannot access the script bound to a document by opening said document with openById(). The answer, instead, covers other issues you had and suggests additional ways to solve your task.
Problem
When trying to use openById() you receive an error message of the following structure:
Exception: Document [doc id here] is missing (perhaps it was deleted, or maybe you don't have read access?)
You mentioned in comments that the id is 58 chars (I bit it is 57) and obtained from "properties", which explains the error - there is no spreadsheet with such an id, because file id and script id you extracted are not the same thing. If you ever need to extract current id programmatically (here it is assumed to be called from a script bound to spreadsheet, but other services have similar methods), you can call getActiveSpreadsheet() -> getId().
Solutions
You stated that you need a maintenance script, so how about creating a standalone script project that is deployed in a way any document can access:
As a library
Any script that has a saved version can be a library that can be used by other scripts by adding its id to the list accessible from Resources->Libraries menu.
As a Web App
Any script that has a doGet, doPost function (or both) can be deployed as a Web App, essentially exposing it to the net. Since you said that the spreadsheets are "produced" by Forms, and you created a "notification" script, I assume you are interested in a FormSubmit event. When you deploy as a Web App, you get a url (don't forget to choose an appropriate permission).
After that, it is only a matter of making sure that:
each Form has an installable onFormSubmit trigger (you can instlall it via [please see refs for docs] ScriptApp.newTrigger('callback name').forForm('form ref').onFormSubmit()).
The callback for the trigger calls the Web App url via UrlFetchApp.fetch() with necessary data (like spreadsheet id or any other info) as query (if using doGet) or request body (if using doPost).
The data you need will be available in the event object constructed on hit to doGet or doPost [note to avoid common misconception: you can't debug event objects in the editor, it can only be done live].
Reference
getActiveSpreadsheet() docs
getId() docs
getFileById() docs
Standalone scripts guide
Bound scripts guide
Libraries guide
Form submit event reference
new Trigger() docs
UrlFetchApp.fetch() docs
doGet / doPost event object docs

how to share same app-script between multiple (different) spreadsheets in google spreadsheets

First of all I am after 3hours of reading docs about google cloud, publishing, projects and so on. After many tries i realized that i am missing something so here is my question.
I have two spreadsheets, lets, call it "prices" and "costs". What i want to achieve is that they share same app scripts and if I change one script, since it is shared by both it will automatically change in secons.
So i did create an app script that returns a string (just for simplicity) in a cell and called it STRINGFUNCTION(); Is is created in PRICES spreadsheet. My goal is to have it working in COSTS file without typing it manually.
I expected that if i click Resources > Cloud Platform Project and add both app scripts from both spreadsheets to the same project it will work automatically. Well, it wont - if i write in a cell =STRINGFUNCTION() in PRICES it works fine, and in COSTS - it says that function in not known.
How can I achieve that so it works between my both files and they share same function if they are both in same project?
You can do this by putting your code in a standalone script and use it as a Library.
https://developers.google.com/apps-script/guides/libraries
Here's how to do this:
Go to https://script.google.com/ and create a new project
Replace the code with the code for your custom functions and save
Click "Untitled Project" and give it a name to use for accessing the library
Click the blue Deploy button and choose New Deployment
Click the gear beside Select type and choose Library
Enter a description and click Deploy (this is what makes your Library available from other scripts)
Go to Project Settings and copy the script ID
Go back to your Spreadsheet, open the script for your sheet, click + beside libraries and paste the script ID and click Add
Remove the code for the custom functions if necessary and you're going to create functions to pass through the library functions
If your function is called STRINGFUNCTION(), create a function in the local apps script like this:
function STRINGFUNCTION(parameter) {
return libraryName.STRINGFUNCTION(parameter);
}
Do that for each function you want to use from the Library and save your code. You will need to authorize permission for the script if you haven't already. Now the custom functions should be available in your spreadsheet. Copy and paste this "pass through" script to each sheet where you want to be able to access the custom functions from the Library.
Custom functions inside a Library cannot be called directly from a sheet. You only have to set up this local script to pass through the functions, once. Now if you update the script in the Library, the updated functions will be available to the sheet. You will need to do the deploy set each time you make a change to the Library to publish the changes. If the changes aren't working in the sheet, click on the Library in the local script and make sure the version is Head or the latest version you published.
You can only bind a Google Apps script file to one document at a time. Apps script doesn't allow you to edit the contents of a .gs file on the cloud from inside another Apps script file, as trying to fetch:
var data = DriveApp.getFileById('script-id').getAs('application/vnd.google-apps.script');
will return the error:
No item with the given ID could be found, or you do not have permission to access it.
You could however bind the script to one spreadsheet, for example say the 'COSTS' spreadsheet, and create a second sheet within the spreadsheet for 'PRICES' with all the relevant function calls. In the separate 'PRICES' spreadsheet you could then use the build-in IMPORTRANGE formula to get the data from the 'PRICES' range from the first spreadsheet that has the bound script.

Cache custom function result between spreadsheet opens

I have a Google spreadsheet that has it's data loaded using a custom function.
The custom function calls a REST API, transforms the data and displays it.
The problem is that every time I open the doc, the function is called and all the REST API calls go out.
I need to be able to manually trigger service calls to refresh data, and in between show the data from last time.
How can I best achieve that?
Short answer
Instead of calling your function from a formula (custom function) call it from:
The script editor
A custom menu
Explanation
At this time custom functions are recalculated when the spreadsheet is open and when any of its arguments changes its values. To run the function at will use the script editor play or debug buttons or a custom menu. Also you could use other triggers. Choose the one that best suit your needs.
If you still want to cache the result of calling the corresponding REST API use the Class Cache.
References
Overview of Google Apps Script
Custom functions in Google Sheets
Cache Service

How can I give my programmatically created Google Spreadsheets a Sidebar?

Background:
I have a Script Project, tooled up using node-google-apps-script
I have a Rails app that uses the Script Project to create Spreadsheets using the REST Execution API. It does this by cloning a template Spreadsheet.
My Rails app also occasionally pushes data to the Spreadsheet, using the REST Execution API
The Script Project is published privately using Publish>Deploy as API Executable..., and the Rails App uses the API Executable ID to talk to it.
The above all works reasonably well, and development is not too onerous.
Now I want the generated spreadsheets to have a sidebar. The alternatives I've discovered so far each seem problematic:
Alternative 1: Publish private Add-On
I'm aware that I can get my client to make me an admin of their google apps domain, and then I can publish the Script Project as a domain-restricted add-on. This alternative seems like it will be onerous both for development & usage:
add-ons take up to an hour to publish. Publishing a version of the Script Project is now two manual routines which can't be automated
as best I can tell, I can't programmatically add the add-on to the spreadsheet. It has to be installed in each spreadsheet before use.
Alternative 2: Give the Template Spreadsheet a Bound Script
I could give the Template Spreadsheet a Bound Script. Apparently, that will be copied. Bound Scripts can't be tooled with node-google-apps-script, so I will be stuck using copy & paste updating. Updates to the script would require manually updating each existing spreadsheet! However possibly I could make the sidebar a skeleton that pulls its actual content from the Script Project.
Is there a better way?
Essentially what you can do is tie all the sidebar creation part of the programming to the library that you have created. In the templated spreadsheet that you use, you should only need to do the following:
Add your library to the container bound script using the Project Key of the library script.
Add a simple onOpen function to create a menu that then calls the library create sidebar function.
Since all of the server side functions that might be called by the sidebar will be contained in the library, you will need to add a function handler that calls the correct function in your library to pass off to the sidebar when using google.script.run For example:
Library Functions:
function getSomeNum() {
return 3;
}
function doSomething(e) {
return e.num * 2;
}
var runFunction = {
getSomeNum : getSomeNum,
doSomething : doSomething
};
function runHandler(functionName, parameters) {
return runFunction[functionName](parameters);
}
Container Bound Handler:
function runHandler(functionName, parameters) {
return MyLibraryIdentifier.runHandler(functionName, parameters);
}
Example google.script.run call from sidebar:
function onLoad() {
google.script.run
.withSuccessHandler(processNum)
.runHandler("getSomeNum");
}
function processNum(num) {
var event = {
num : num
};
google.script.run
.withSuccessHandler( function(retNum) { alert(retNum); } )
.runHandler("doSomething", event);
}
Keep in mind that any user that needs to utilize this sidebar must at least have view access to the library. What I ended up doing was deploying as an API Executable for anyone and then setting the sharing of the script to public-view only, but must have link. It's the only thing that seemed to work to get it to multiple users.
Another downside is when updating the code of the library, the container bound scripts will not be updated unless you change the library version they are utilizing. Meaning the user would have to actively look for newer library versions. When Updating the API executable, the menu gives you the impression that you can just update the current version, but I haven't had any luck in to actually getting it to reflect the changes (perhaps I just wasn't patient enough).
This should at least get you a base line of where to begin your testing.

How can I invoke my standalone script in my spreadsheet?

I have created a standalone Apps Script from Google Drive, but when I try to access it from a Google Spreadsheet, I don't see a way to access the script (even though when I created the script, I created it as a spreadsheet script).
In the spreadsheet, the "Tools->Script manager..." menu item doesn't show my script, nor does the "Tools->Script editor...". The latter has a section for 'recent projects', but it doesn't list my newly created script, nor its associated project.
If I create a new script from inside the spreadsheet (i.e .Tools->Script editor...) and cut and paste the code from the standalone script, it works fine. However, this can only be used in the single spreadsheet -- to use it from another one, I have to cut and paste / go through auth again.
Is there a way to do this without publishing the script to the gallery, which I assume makes it public (I don't have a problem making it public, but it seems like there must be a better way)? The script is here -- it is world readable with the link. It adds a menu to a google spreadsheet that lets you run a query in BigQuery and dumps the results into your spreadsheet.
The behavior you describe is exactly how it is intended to work... the script you wrote can only work if it is bounded to a spreadsheet.
The fact that you wrote it in an independent script makes it indeed a standalone app but it can only be executed from within the script editor and in this context an onOpen function like the one you wrote doesn't make sense (it runs on opening a document/spreadsheet, not the script).
For now a script can only be bounded to a spreadsheet by copy/pasting the code, at least a script that you want to use as you do (with a menu and direct function calls and activeSheet calls), maybe one day Google will expand the concept of libraries to make your script function available from different document directly (read also the doc about libraries).
There are actually 2 types of standalone Apps Scripts, the one you tried that runs only from within the editor and webapps that are deployed and run in a browser window by themselves. The latter are always build around a doGet function that represents an entry point for the application. You should read the documentation on the subject to get all the details.
To summarize : if you want to access your spreadsheet using getActiveSpreadsheet() and similar methods then you have to write (or paste) the script in the script editor bounded to the spreadsheet.
If you want to have a standalone script you can also interact with spreadsheets (and other document as well) but you have to access them by their ID and the spreadsheet will only be viewable from another browser window, without link between them, and there will be no "active spreadsheet" nor "active sheet"...
A webapp can of course also do that with the advantage of a user interface that you can adapt to your needs but this is becoming a bit too far from the scope of your question.
Hoping I've made it (a bit) more clear, at least I tried to make it simple ;-)
I worked this out with Jordan, following the instructions here:
https://developers.google.com/apps-script/guide_libraries
First, he had to share his standalone script with me, so I had read access to it (probably setting the script as public read would work too).
Then he told me his project key.
I took his project key and inserted it in "Find a Library", in the "Manage Libraries" menu.
For this to work, Jordan had to export "a version" of his library - so third parties can rely on stable code and upgrade at their own rhythm.
As Jordan's script runs automatically on onOpen(), my code had to define a function onOpen() that called his script onOpen(). To do this, the library manager gave me an identifier for the imported library.
Sample code once the library is imported and the library manager gives you an identifier for the library:
function onOpen() {
GivenLibraryIdentifier.onOpen();
}
You can also use installable triggers. If you install an installable trigger (such as onOpen) from your standalone script into the document, it gets similar privileges as if it were a simple trigger.
And better yet, when the installable trigger calls its function from your standalone script, it executes WITHIN your standalone script! So it can call any other function in your standalone script. Its Logger.log() entries can even be viewed from your standalone script, even though it was executed from your sheets document.
So basically, if your script installs an onOpen() trigger tied to one of its functions, it's as good as if your Sheets document had required() your whole standalone script file.
(Assuming the owner of the script has edit privileges to the file.)