How can I give my programmatically created Google Spreadsheets a Sidebar? - google-apps-script

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.

Related

Control Multiple Google Sheets through a common Library instead of code.gs [duplicate]

I have a google apps script that I want to use in multiple documents. I also may want to change it later in those documents, so it is imperative that I use the same script in all those documents, and not copies of that script.
I am aware of the below question, which may qualify as a duplicate, but I am reluctant to accept its answer.
Google Apps Script - How To Have One Script In multiple containers?
So, my question is: is there really no way to share a script among multiple google documents? Must you really create a new script project for every document and copy-and-paste the code from an other? Moreover, if you fix a bug in one of them, do you have to remember which documents use that script and open the script editor in each of them and copy-and-paste the new code?
Libraries were designed specifically for this situation... please have a look at the documentation here.
I have come up with a solution that works for me. It allows keeping any number of scripts attached to a sort of master document (let's call it MyScripts). No libraries, no publishing required.
Create a document and name it MyScripts (or whatever). The document's body can be empty, or you could write some instructions there. Then, paste the following code into MyScript's script editor:
// adds a menu to the master document's UI
function onOpen() {
DocumentApp.getUi()
.createAddonMenu()
.addItem('Do something', 'doSomething')
.addItem('Something else', 'somethingElse')
.addToUi()
}
// returns the target document based on its URL
// may be tweaked in order to use the documentId instead
function findDoc(prompt) {
var ui = DocumentApp.getUi();
var pro = ui.prompt(prompt, 'Document URL:', ui.ButtonSet.OK);
var url = pro.getResponseText();
return DocumentApp.openByUrl(url);
}
function doSomething() {
var doc = findDoc('Do something');
if (doc) {
// do something with the target document
var len = doc.getBody().getText().length;
DocumentApp.getUi().alert('The document is ' + len + ' characters long')
}
}
function somethingElse() {
var doc = findDoc('Something else');
if (doc) {
// do something else
}
}
The onOpen() function should be self explanatory.
findDoc() is the real hack. It prompts the user for the URL of the target document, the document we want to act on. If the URL is valid, then findDoc() returns the corresponding document object.
The last two functions are just stubs and should be replaced with your own code, but notice how findDoc() gets called at the beginning of each.
When you want to run a script against a document, copy its URL, then open MyScripts, choose the corresponding Add-Ons menu item, paste the URL and click OK.
Please notice that you will get a scary warning message the first time you attempt to run a script this way. Just be sure that your doSomething(), your somethingElse(), etc. only contain safe code before ignoring the warnings and executing the scripts.
As of September 6, 2020, using a library implies to create a project to add the reference to the library and some code to make the library functions available to the container document. The only way to have a script available on multiple documents without having to create an script on them and without limitations is by creating a G Suite Editor add-on.
As of December 2019 all the G Suite editor add-ons are published to the G Suite Marketplace. You could make an add-on unlisted and only users having G Suite accounts could published add-ons limited to be visible by other users from the same domain.
Test as add-on
If you don't want to publish and add-on you might use the Run > Test as add-on but there are several limitations. I.E. the following can't be used on this mode:
Triggers
Custom Functions
Master project
As suggested on Giussepes' answer, is to use a "master project" to hold your scripts. This also has several limitations
Most of the active methods can't be used
Simple triggers can't be used but it's possible to programatically create installable triggers
Custom functions only works on spreadsheets add-ons and scripts bounded to the spreadsheet that will use the custom function
Be resigned
If you are resigned to have a project on each of your documents you could reduce the burden of keeping the scripts updated by using CLASP or the Goole Apps Script Assistant for GitHub
Resources
Builting editor add-ons | G Suite
Command line interface using clasp | Google Apps Script
Sorry, my reputation was too low to add a comment. :(
In Giuseppe's answer use this for find docs - minor change
function findDoc(pros) {
var ui = DocumentApp.getUi();
var pro = ui.prompt(pros, 'URL:', ui.ButtonSet.OK);
var url = pro.getResponseText();
return DocumentApp.openByUrl(url);
}

Using Google Apps Script Libraries

I have read all Google documentation on managing and creating libraries, yet I still do not know if they are an appropriate option for the problem I am trying to solve.
I know how to save a version of a standalone script. I know how to add the library to a spreadsheet via the script editor. But I don't understand, very simply, how to trigger the library script within the new spreadsheet.
I have a spreadsheet that serves as an often-copied template within my organization. The template contains a script that (onOpen) accesses data on a separate spreadsheet (a master database) and sets those values on a tab called "admin." The desired result is to have a copy of the master database living within the template sheet (and every subsequent copy of the template sheet). At this point, there are thousands of copies of the template sheet, each running that same script.
Whenever I have to change the script, I have to change it within thousands of sheets. Can I use a library instead? I'd like to be able to create a new version of the script in the library and have all sheets connected to that library experience the change. I understand that the library needs to be in development mode (within each sheet) to do this. I also understand that in order to make this switch, I will probably still have to go into each sheet to add the library. I'm just hoping it will be the last time I have to do such a tedious task.
Any advice or links to solid info is appreciated.
besides making an add-on (already covered in another answer) I will answer your libraries question. They will work for you. What you are missing is the "connect" part.
For this you want to trigger the library code from say, onOpen. The onOpen in the library is not enough and not detected by apps script. Instead each of your spreadsheet's script needs an onOpen(e) which just calls yourlibrary.onOpen(e).
since those "hook" calls rarely change, specially once you stabilize your library api, and using it in "development" mode will let you modify just the library.
whenever one of those hooks needs to change (say a callback from an html GUI needs a new parameter) you need to update all the spreadsheets. to avoid this, make all your callbacks receive a single json object instead of multiple parameters.
Sorry if I am repeating other answers, but I would like to sum up and add something:
You can access your library functions as follows:
From the app using the library you go to the Resources/Libraries. You can see the library name under "Identifier". On the same line where you can select Development mode.
Library name found in resources
Now in your library you have for example a function
function onOpen(e)
{
Browser.msgBox("HELLO!");
}
In the spreadsheet app you wish to access it you use the library name found in the resources, for example "testlibrary"
function onOpen(e)
{
testlibrary.onOpen(e);
}
Now if you have development mode on, the modifications to functions in the library update automatically to your application (spreadsheet) as long as the user using the application has edit access in your library script.
If anyone using your spreadsheet has a restricted access to your library script (meaning only view access) or development selection is off in the application, you have to go to the application's script, Resources/Libraries and select the most recent version of your library to be used in the app everytime you update the library and save a new version of it.
Still, especially if you are using mostly only the onOpen function , I would recommend using the library rather than copy-pasting the function to the script of each spreadsheet, as it is easier to track which scripts are up to date and it is easier to avoid errors and differences between the scripts.
Even in the more restricted case, if you update function in library - as long as you are already calling it in the app - all you have to do is select the new version of the library used.
I hope I had anything to give in this conversation and my language was appropriate, this was my first answer..
A good question Melly. I've been through a bunch of the documentation and some tutorials on this subject but haven't tried adding any libraries yet. My understanding is that once you are connected to a library all you have to do is call the applicable functions. That once a library is connected the functions in the library become an extension of all the other Apps Script classes available in the script editor.

Using OAuth2 flow within Google Script Library is returning 'Redirect or Missing URI'

I am writing a custom google scripts library for use within multiple google sheets. It uses the Google Script OAuth2 library to manage the OAuth flow with the Smartsheet API. Unfortunately it is returning 'Redirect URI missing or invalid'.
I had the OAuth2 flow working well in a single spreadsheet script, but it stopped working when I moved the code into a library.
I DID update the redirect URI within Smartsheet's developer menu and I verified that the URI returned from .getRedirectUri() was the one associated with my app within Smartsheet's app registration.
The other aspects of the code within the library are working well, and give me high confidence that it is still working as it did when it was tied to a sheet.
My goal with the library is that it can be used from multiple sheets to access the Smartsheet API, and thereby prevent me from creating a unique app (with unique redirect URI) within the Smartsheet developer menu for each sheet that might ever use this code.
Is this possible? What might I be doing wrong that it doesn't like the redirect URI?
When developing a library it is best to be agnostic to the source of token. A common design pattern is to have the user pass in a function that your library calls to get a token. This allows the user to choose how and where the OAuth workflow is implemented.
myLibrary.gs
var tokenService_ = function(){return null};
function setTokenService(tokenService){
tokenService_ = tokenService
}
//some hypothetical function in your library
function getData(options){
return fetchFromAPI(options,tokenService_());
}
userCode.gs
function requestData(){
myLibrary.setTokenService(function(){ return ScriptApp.getOAuthToken()});
var data = myLibrary.getData("foo");
}

Can a Google Spreadsheet Apps Script library contain a user dialog?

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.

Call a Web App script from within a spreadsheet script?

How can I call an external Google Web App script from within my script in a Google spreadsheet container? I have a number of spreadsheets that are created dynamically and would like them all to call a single Web App for uniformity and ease of maintenance.
Have a look at this blog post: Trigger updates between master – slave spreadsheets
Basically what you want to accomplish in the same, in the specific scenario the master spreadsheet is actually a web app (web service) for all the slave spreadsheet documents.
You are describing a textbook use of Google Apps Script Libraries! Start by reading Introducing Versions and Libraries in Apps Script.
Your master script, plus any utilities you want to access, needs to be set up as a library, for example let's say it's called MyLib. Your dynamically created (client) spreadsheets then access functions that are in MyLib by treating it as a javascript object. For example, a function calcValues() would be referenced like this:
...
var result = MyLib.calcValues(dataRange);
...
You didn't ask this, but just in case you're thinking of using custom functions (as described in Custom Functions in Spreadsheets) that are implemented in your library / master script, you will need to provide wrapper functions in your client spreadsheets. Here's that same example as a wrapper:
function calcValues (dataRange) {
return MyLib.calcValues(dataRange);
}