How do I determine what caused my script to run? - google-apps-script

Whenever I attempt to display a UI dialog (e.g. msgBox or alert) it works fine when invoked via a menu item (e.g. from Google Sheets), but it hangs my script if I try to invoke it from the Google Apps Script editor (e.g. via Run > Run function).
My guess is it's because the Google Apps Script editor can't display any UI. To resolve this, I'd like to create a wrapper function that checks how the script was run, and not present UI depending on the source.
The "Executions" screen has the notion of Type (Editor, Standalone, Trigger):
This makes me think there is a way to get this type in code somehow.
Psuedo code of what the function might look like:
function showMessage(message) {
var scriptSource = ???;
if (scriptSource === "Standalone") {
Browser.msgBox(message);
} else {
console.log(message);
}
}
How would I get the scriptSource?
The closest thing I can find is TriggerSource, but that is missing the enum values 'Editor' and 'Trigger'. Furthermore, it's a property only available on a Trigger. I don't know how to access the current trigger. From my understanding, that's only available via the event object (e.g. via triggerUid) on functions acting as triggers. This method I'm running in the apps script editor doesn't have access to an event object.

Not the best solution, but my current workaround is to create 3 versions of each function, and append how it was invoked to the name.
For example, if there was a "Hello World" function:
function onOpen() {
var menu = [
{name: 'Hello World', functionName: 'helloWorldViaMenu_'},
];
SpreadsheetApp.getActive().addMenu('Custom', menu);
}
function helloWorldViaMenu_() {
helloWorld_(false);
}
function helloWorldViaEditor() {
helloWorld_(true);
}
function helloWorld_(invokedFromEditor) {
if (invokedFromEditor) {
Logger.log("Hello world");
} else {
Browser.msgBox("Hello world");
}
}
helloWorldViaEditor is the only that doesn't have a _ at the end so it can be selected via the "Select function" Editor UI dropdown.

You want to know whether the current project is the container-bound script type or the standalone script type.
You want to use Browser.msgBox().
I could understand about your question as above. In order to achieve it, as a workaround,I would like to propose to use Apps Script API. The flow of sample script is as follows. I think that there are several workarounds for your situation. So please think of this as one of them.
Retrieve the parent ID of the project using the method of projects.get in Apps Script API. The parent ID means that the file ID of Google Docs.
When the parent ID is returned, it is found that the project is the container-bound script type.
When the parent ID is NOT returned, it is found that the project is the standalone script type.
When the mimeType of parent ID is Google Form, Browser.msgBox() cannot be used. So the if statement is used for this.
Sample script:
This is a sample script. In this sample script, the script ID of current project is used. Of course, you can also manually give the script ID.
var id = ScriptApp.getScriptId(); // Retrieve scriptId of current project.
var url = "https://script.googleapis.com/v1/projects/" + id + "?fields=parentId";
var res = UrlFetchApp.fetch(url, {headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}});
res = JSON.parse(res.getContentText());
if ("parentId" in res) {
Logger.log("Container-bound script type.")
var mimeType = DriveApp.getFileById(res.parentId).getMimeType();
if (mimeType === MimeType.GOOGLE_FORMS) {
Logger.log("Browser.msgBox() cannot be used at Google Form.");
} else {
Browser.msgBox("Hello world");
}
} else {
Logger.log("Standalone script type.")
Logger.log("Hello world");
}
Note:
When you use this script, please do the following flow.
Enable Apps Script API at API console.
At least, add the following scopes to the manifests.
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/script.external_request
https://www.googleapis.com/auth/script.projects.readonly
If in your script, other scopes are required to be added, please add them. And if you want to use the automatically installer of scopes with the script editor, you can achieve it using a library. You can see the detail information at here.
References:
Apps Script API
Manifests
projects.get
Taking Advantage of Manifests by GAS Library
If I misunderstand your question, I'm sorry.
Edit:
You want to confirm whether the function is called from the script editor or the custom menu.
If my understanding is correct, how about this sample script? This is a sample script. The process list can be retrieved by giving the script ID and function name. In this sample script, using "ProcessType" of processes.listScriptProcesses in Apps Script API, it confirms whether the function is called from the script editor or the custom menu.
Sample script:
This is a sample script. The process list can be retrieved by giving the script ID and function name.
When you use this script, please enable Apps Script API at API console, and add a scope of https://www.googleapis.com/auth/script.processes to the manifests.
The how to use this script is as follows.
Run addCustomMenu().
Run sampleFunction at the custom menu.
By this, Call from custom menu is shown in log.
Run sampleFunction at the script editor.
By this, Call from script editor is shown in log.
Script:
function addCustomMenu() {
SpreadsheetApp.getUi().createMenu('sampleCustomMenu').addItem('sample', 'sampleFunction').addToUi();
}
function sampleFunction() {
var scriptId = ScriptApp.getScriptId();
var functionName = "sampleFunction";
var url = "https://script.googleapis.com/v1/processes:listScriptProcesses?scriptId=" + scriptId + "&scriptProcessFilter.functionName=" + functionName;
var res = UrlFetchApp.fetch(url, {headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}, muteHttpExceptions: true});
res = JSON.parse(res);
if (!("processType" in res.processes[0])) {
Logger.log("Call from custom menu")
} else if (res.processes[0].processType == "EDITOR") {
Logger.log("Call from script editor")
}
}
References:
Apps Script API
Manifests
processes.listScriptProcesses
ProcessType

Making Dialogs
You can run them from the menu or the script editor. They work the same.
function makeAmenu(){
SpreadsheetApp.getUi().createMenu('A Menu')
.addItem('Run my Dialogs', 'showMyDialogs')
.addToUi();
}
function showMyDialogs(){
var ui=SpreadsheetApp.getUi();
ui.alert('This is an alert');
ui.prompt('This is a prompt');
var html=HtmlService.createHtmlOutput('<p>This is a modeless dialog</p><input type="button" value="Close" onClick="google.script.host.close();" />');
ui.showModelessDialog(html, 'Dialog');
}
If you run a script from here:
The you have to go here to see it:

Related

Can't Get Specific .html to load on doGet() | WebApp

I am trying to load a specific .html depending on the current cell's Text (either .value() or .getdisplayValue()).
However, for some reason only the Payroll.html file is ever being fired, even though the current cell's text is "Click To Upload Receipt"
function doGet(e) {
if (sheet.getActiveCell().getValue() === "Click To Upload Receipt") {
var htmlOutput = HtmlService.createTemplateFromFile('UploadFile');
} else {
var htmlOutput = HtmlService.createTemplateFromFile('Payroll');
}
htmlOutput.message = '';
return htmlOutput.evaluate();
}
I have ReDeployed my WebApp and set the URL of this as a link on the current cell.
Please let me know if you need any specific information to assist. Thank you all.
An Apps Script webapp (container bound or not) has no concept of an "active cell," in the doGet or doPost event handler.
Instead, you should invoke the doGet endpoint with a URL query parameter containing the desired template to display. You can access this from the event object received by the doGet call.
For example,
https://scripts.google.com/a/.......?action=do%20payroll
This would invoke your doGet handler with a function argument that has a property "parameter" with the property named "action" and the value "do payroll".
You could then load the desired parameter by inspecting this value, and provide a fallback if an unknown value is provided:
function doGet(eventObj) {
const action = eventObj.parameter.action;
if (action === "do payroll") {
// Do stuff
} else if (action === "do receipt") {
// Do other stuff
} else {
// Provide fallback for incorrect user input
}
}
This may be helpful: https://developers.google.com/apps-script/guides/web
Despite that sheet has not being defined, getActiveCell() doesn't work as you assumed for Google Apps Script web apps, but it might work for a dialog of sidebar.
The above because the web application instance hasn't the "active" context. If web app code belongs to a spreadsheet bounded project, SpreadsheetApp.getActiveSpreadsheet() will return the bounded spreadsheet but methods like getActiveCell and getActiveRange will return A1 of the first sheet.
Instead you using the "active cell" you could include a query string (i.e. ?page=page_name
A very simplistic way to implement this:
function doGet(e){
const template = HtmlService.createTemplateFromFile(e.parameter.page || 'index');
return template.evaluate();
}
Related
Linking to another HTML page in Google Apps Script
Multi-page site in Google Apps Script: How to invoke doGet() through jquery or javascript?
Resources
https://developers.google.com/apps-script/guides/web

Google Apps Script: What is the correct way to check if I have access to a Google Spreadsheet by URL

I am currently writing a Google Apps Script inside a Google Sheet to read data from a list of spreadsheets (spreadsheet url is provided by the user). However, I cant seems to find a way to check if the url is valid or if user have access to the spreadsheet or not before calling SpreadsheetApp.openByUrl().
I have written the following code to "validate" the url:
for(int i = 0; i < urls.length; i++) {
let spreadsheet = null
try {
spreadsheet = SpreadsheetApp.openByUrl(urls[i]);
} catch (e) {
continue;
}
// Continue to do other stuff to read data from spreadsheet...
}
This however has an issue, it was able to catch the first few 'You do not have permission to access the requested document.' exception. But after a certain number of exception had occur, I would get a permenant error that cant be caught, stopping the script all together.
Is there a better way to do this?
Minimal reproducible example:
Create 3 google sheet using different google account
Using a different google account, create a google sheet and add the following code into Code.gs
function myFunction() {
// Put any 3 real spreadsheet url that you do not have access to
let urls = [
"https://docs.google.com/spreadsheets/d/1gOyEAz0amm4RghpE4B7f26okU3PG3vWZkrfiC-SBlbw/edit#gid=0",
"https://docs.google.com/spreadsheets/d/1Oia7ADu5BmYroUq1SLyDMHTJowrwSXOhCEyNO3nXmMA/edit#gid=0",
"https://docs.google.com/spreadsheets/d/1HE_IXURpBr_FJN--mwLo6k9gih07ZEtDGBqYSk6KgiA/edit#gid=0",
]
urls.forEach(url => {
try {
SpreadsheetApp.openByUrl(url)
} catch (e) {
console.log("Unable to open this spreadsheet")
}
})
}
function onOpen() {
SpreadsheetApp.getUi().createMenu("Test").addItem("myFunction", "myFunction").addToUi()
}
Run the function once in the apps script panel and authorize the application
Refresh this google sheet
Wait for the Custom Menus to show up and press "Menu" > "myFunction"
As you can see, the openByUrl() call is sitting inside the try catch block, however when you run the function through custom menu, you will still get "Error: You do not have permission to access the requested document.".
Executions Log:
From your question, I thought that your situation might be due to the specification or a bug of SpreadsheetApp.openByUrl. If my understanding is correct, in order to avoid this issue, how about putting the method for checking whether the file can be used before SpreadsheetApp? In your script, how about the following modification?
From :
SpreadsheetApp.openByUrl(url)
To:
var fileId = url.split("/")[5];
var file = DriveApp.getFileById(fileId);
spreadsheet = SpreadsheetApp.open(file);
In this modification, the file is retrieved with DriveApp.getFileById(fileId). When fileId cannot be used, an error occurs. But in this case, try-catch can be correctly worked. By this, the issue of SpreadsheetApp doesn't occur.

Can I use DocumentApp.openById() with read only permission?

I am creating a Google Apps Script add-on that is for a Google Spreadsheet, but it needs to be able to access the content of a separate Google Doc, which I am doing using DocumentApp.openById(). I have given the script these scopes:
"oauthScopes": [
"https://www.googleapis.com/auth/documents.readonly",
"https://www.googleapis.com/auth/script.container.ui",
"https://www.googleapis.com/auth/spreadsheets.currentonly"
]
But apparently, that's not enough. The script is telling me it needs the https://www.googleapis.com/auth/documents permission to work properly. However, it seems excessive to give the add-on permission to edit ALL Google Docs files when it just needs to be able to view the content of one. Am I missing something? Is there a way to give it read-only access to a separate Google Docs file?
Here is the function I am using for testing, with most of the document ID censored out:
function getDoc() {
var id = '1NLH----------------------------------------'
var templateFile = DocumentApp.openById(id)
var templateText = templateFile.getBody().getText()
Logger.log(templateText)
}
Thanks!
I believe your goal as follows.
You want to retrieve the text data from Google Document using the following script.
function getDoc() {
var id = '1NLH----------------------------------------'
var templateFile = DocumentApp.openById(id)
var templateText = templateFile.getBody().getText()
Logger.log(templateText)
}
You want to achieve this using the scope of https://www.googleapis.com/auth/documents.readonly and Google Apps Script.
Issue and workaround:
In the current stage, DocumentApp.openById of Document service is used, it is required to use the scope of https://www.googleapis.com/auth/documents. It seems that this is the current specification. So, in this answer, as a workaround, I would like to propose to use Google Docs API instead of Document service. When Google Docs API is used, your script can be achieved using the scope of https://www.googleapis.com/auth/documents.readonly.
When your above script is modified using Google Docs API, it becomes as follows.
Sample script:
Before you use this script, please enable Google Docs API at Advanced Google services. This script can work with only the scope of https://www.googleapis.com/auth/documents.readonly.
function myFunction() {
const documentId = "###"; // Please set the Document ID.
const obj = Docs.Documents.get(documentId);
const text = obj.body.content.reduce((s, c) => {
if (c.paragraph && c.paragraph.elements) {
s += c.paragraph.elements.map(e => e.textRun.content).join("");
}
return s;
}, "");
console.log(text)
}
Reference:
Method: documents.get

Using triggers with custom functions on Google Apps Script

I had a prob with my script, which was greatly answered in this question.
Basically custom functions cannot call services that require authorization. However, as far as I understood if I use simple triggers, such as onEdit it could work.
I checked the documentation suggested in the previous question, however I wasn't successful applying that to my code, which you can see below:
function FileName (id) {
var ss = DriveApp.getFileById(id);
return ss.getName();
}
How could I adapt my code to use simple triggers?
Here is a sample sheet that replicates the problem.
I believe your goal as follows.
You want to use your function of FileName as the custom function of Google Spreadsheet.
You want to automatically retrieve the filename when the file ID is put to the column "B".
You want to put the filename to the column "C".
Issue and workaround:
Unfortunately, when the custom function is used, in the current specification, the most methods except several methods (for example, one of them is UrlFetchApp.) that the authorization is required cannot be used. By this, DriveApp.getFileById(id) in your script cannot be used with the custom function. But there is a workaround. At the custom function, UrlFetchApp can be used. In this answer, I would like to propose to use the Web Apps with UrlFetchApp as the wrapper for authorizing. By this, the authorization can be done with the Web Apps. So your function can be run by the custom function.
Usage:
1. Prepare script.
Please copy and paste the following script to the script editor and save it.
const key = "samplekey"; // This is a key for using Web Apps. You can freely modify this.
// This is your function.
function FileName_(id) {
var ss = DriveApp.getFileById(id);
return ss.getName();
}
// Web Apps using as the wrapper for authorizing.
function doGet(e) {
let res = "";
if (e.parameter.key === key) {
try {
res = FileName_(e.parameter.id);
} catch (err) {
res = `Error: ${err.message}`;
}
} else {
res = "Key error.";
}
return ContentService.createTextOutput(JSON.stringify({value: res}));
}
function Filename(id) {
const webAppsUrl = "https://script.google.com/macros/s/###/exec"; // Please set the URL of Web Apps after you set the Web Apps.
const res = UrlFetchApp.fetch(`${webAppsUrl}?id=${id}&key=${key}`);
if (res.getResponseCode() != 200) throw new Error(res.getContentText());
return JSON.parse(res.getContentText()).value;
}
2. Deploy Web Apps.
On the script editor, Open a dialog box by "Publish" -> "Deploy as web app".
Select "Me" for "Execute the app as:".
By this, the script is run as the owner.
Select "Anyone, even anonymous" for "Who has access to the app:".
In this case, the access token is not required to request to Web Apps. But in this sample script, a key for requesting to Web Apps is used.
Click "Deploy" button as new "Project version".
Automatically open a dialog box of "Authorization required".
Click "Review Permissions".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Click "OK".
Copy the URL of Web Apps. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
Please set the URL of https://script.google.com/macros/s/###/exec to url of above script. And please redeploy Web Apps. By this, the latest script is reflected to the Web Apps. So please be careful this.
3. Test this workaround.
When the file ID is put to the cell "A1", please put =filename(A1) to a cell as the custom function. By this, the script is run and the response value is returned.
Note:
Above sample script is a simple sample script for testing your script. So when you want to use the various methods, this post might be useful.
Please use this script with enabling V8.
As other method, I think that when the file ID is manually put to the column "B", the installable OnEdit trigger can be used. The sample script is as follows. Please set the sheet name. And please install the trigger to the function of installedOnEdit. Ref By this, when the file ID is put to the column "B" of sheetName, the file ID is put to the column "C".
function installedOnEdit(e) {
const sheetName = "Sheet1";
const range = e.range;
const sheet = range.getSheet();
if (!(sheet.getSheetName() == sheetName && range.getColumn() == 2 && range.getRow() > 1)) return;
const value = range.getValue();
let res = "";
try {
res = DriveApp.getFileById(value).getName();
} catch(e) {
res = e.message;
}
range.offset(0, 1).setValue(res);
}
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
Enhanced Custom Function for Google Spreadsheet using Web Apps as Wrapper
Related questions
Can you write a Google Sheets function that draws something?
Error when running Youtube Data Service in App Scripts (js) – Daily Limit for Unauthenticated Use Exceeded
How to enable not authorized users to protect the spreadsheet
Changing Owner of the Sheet irrespective of the duplicator
Installable Triggers
As you can draw from the documentation, simple triggers cannot access services that require authorization neither
You have to use installable triggers instead.
However the workflow is very different from custom functions.
In your specific case, you can implement e.g. that when a cell in column A is being edited (that is a new URL is being inserted) - the respective file name is being found and returned into column D.
You can retrieve the value and the row in which the new URL is being inserted with help of event objects.
Sample:
function FileName (event) {
var id = event.value;
var ss = DriveApp.getFileById(id);
var row = event.range.getRow();
var sheet = event.range.getSheet();
// for column D:
var column = 4;
var returnCell = sheet.getRange(row,column);
returnCell.setValue(ss.getName());
}
For using an installable onEdit trigger - bind it to this function through going on Edit > Current project's triggers as explained here.

Using a variable from another script in the same document (apps scripting)

I am trying to access a variable from another script A.gs in script B.gs. they are both in the same document. How could I do this?
I am not sure how I should solve this problem, I am a beginner with apps scripting and I can't find anything on the internet about it.
code.gs:
ui = DocumentApp.getUi();
function onOpen () {
A = prompt('Hello');
}
code2.gs:
function onOpen () {
if (A === "123") {
ui.alert('Hello')
}
}
I want Hello to be output if 123 is entered into the prompt, but when I try to run the code I get the error:
ReferenceError: "A" is not defined. (line 3, file "code2")
In your situation, code.gs and code2.gs are in a project of a container-bound script type of Google Document.
If my understanding is correct, how about this answer? Please think of this as just one of several answers.
Modification points:
In your script, the scripts of code.gs and code2.gs are used as one project at the script editor. So in your script, there are 2 same functions of onOpen() in the project. In this case, only one of them is run. In your case, onOpen() of code2.gs is run and the error of ReferenceError: "A" is not defined. occurs.
Modified script:
If you want to modify your script and you want to work the functions when the Google Document is opened, how about the following modification?
1. Copy and paste the following script to code.gs and code2.gs of the script editor:
code.gs:
var ui = DocumentApp.getUi();
function installedOnOpen () {
A = prompt('Hello'); // or ui.prompt('Hello').getResponseText();
sample(A);
}
code2.gs:
function sample (A) {
if (A === "123") {
ui.alert('Hello')
}
}
Or, if you want to run independently 2 functions, how about the following modification? In this modification, the value is saved using PropertiesService.
code.gs:
var ui = DocumentApp.getUi();
function installedOnOpen () {
A = prompt('Hello'); // or ui.prompt('Hello').getResponseText();
PropertiesService.getScriptProperties().setProperty("key", A);
}
code2.gs:
function sample () {
var A = PropertiesService.getScriptProperties().getProperty("key");
if (A === "123") {
ui.alert('Hello')
}
}
Or, you can also modify as follows. But, in your situation, this might not be required.
function installedOnOpen () {
var ui = DocumentApp.getUi();
var A = ui.prompt('Hello').getResponseText();
if (A === "123") {
ui.alert('Hello');
}
}
2. Install OnOpen event trigger:
In order to run the function of installedOnOpen when the Google Document is opened, please install the OnOpen event trigger to the funciton of installedOnOpen as the installable trigger.
3. Run script:
In your case, there are 2 patterns for running script.
Pattern 1:
Open Google Document.
Pattern 2:
Run installedOnOpen at the script editor.
By above, installedOnOpen is run. And you can see the dialog at Google Document.
Note:
This modification supposes that the function of prompt() returns the value of 123 as the string value.
If you cannot provide the script of prompt(), as a test case, how about modifying from prompt('Hello'); to ui.prompt('Hello').getResponseText();?
References:
Access a variable across multiple script file under a GAS project
Also I think that this thread might be useful for you.
Installable Triggers
If I misunderstood your question and this was not the direction you want, I apologize.
As I can see you define onOpen twice. It does not make sense.
You also don't declare variables and this is reflected in the style of your code. Try declaring the variables and you will realize that your code has no effect.