I'm trying to write a polling web app that checks Google Drive and automatically downloads files without user interaction.
Using ContentService I have managed to get things working when I place the code in the doGet function.
However this only works once and there does not appear to be a way to refresh or reload the page automatically on a timer event.
Using a SetTimeout on the client side javascript I can get a function on the server side to automatically trigger at certain intervals but then I am stuck with what to do with the output from ContentService.
The on Success call back will not accept the output from createTextOutput.
My solution does not not need to be deployed and I'm happy to execute from the editor if that expands my choices.
So once I have the output from createTextOutput on my server side what am I supposed to do with it to get it back to the client in order to cause the file download?
I have included the code if that helps.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
setTimeout(
function ()
{
document.getElementById('results').innerHTML = 'Event Timer';
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onFailure)
.fetchFromGoogleDrive();
}, 60000);
function onSuccess(sHTML)
{
document.getElementById('results').innerHTML = 'File Downloaded ' + sHTML;
}
function onFailure(error)
{
document.getElementById('results').innerHTML = error.message;
}
</script>
</head>
<body>
<div id="results">Waiting to DownLoad!</div>
id="Fetch">Fetch!</button>
</body>
</html>
function doGet() {
Logger.log('doGet');
return HtmlService.createHtmlOutputFromFile('form.html');
}
function fetchFromGoogleDrive() {
//Logger.Log('fetchFromGoogleDrive');
var fileslist = DriveApp.searchFiles("Title contains 'Expected File'");
if (fileslist.hasNext()) {
//Logger.Log('File found');
var afile = fileslist.next();
var aname = afile.getName();
var acontent = afile.getAs('text/plain').getDataAsString();
var output = ContentService.createTextOutput();
output.setMimeType(ContentService.MimeType.CSV);
output.setContent(acontent);
output.downloadAsFile(aname);
return afile.getDownloadUrl();
}
else
{
//Logger.Log('No File Found');
return 'Nothing to download';
}
//Logger.log('All files processed.');
}
EDIT: Different answer after clarification.
If this is intended to run automated as a webapp what I would do is return the getDownloadUrl and create a new iFrame using that that as the source.
Apps Script
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function getDownloadLink(){
//slice removes last parameter gd=true. This needs to be removed. slice is a hack you should do something better
return DriveApp.getFileById("0B_j9_-NbJQQDckwxMHBzeVVuMHc").getDownloadUrl().slice(0,-8);
}
index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p id="dlBox"></p>
</body>
<script>
function buildLink(res){
var dlBox = document.createElement("iframe");
dlBox.src = res;
document.getElementById("dlBox").appendChild(dlBox)
}
//automate this as you need
google.script.run
.withSuccessHandler(buildLink)
.getDownloadLink();
</script>
</html>
Related
I am testing the app script platform and I have a doubt when using this code called from HTML file:
JSON.parse(<?= JSON.stringify(getDataFromSheet("tyreUse", "valueSearched")); ?>);
If I set the string value directly it works.
If I try to pass a variable that is declared in it does not recognize it. How can I pass a JS variable to the app script function like next example?
let value_searched = "cars";
JSON.parse(<?= JSON.stringify(getDataFromSheet("tyreUse", value_searched)); ?>);
Scriptlets like <?= ?> are used in html templates to load data from the server into html pages prior to rendering. If you want to pass data back to a server side function then you can use google.script.run and there are restrictions on the data types that you can pass.
google.script.run
Here is an example of getting data from spreadsheet dynamically. I typically build my page and then use an anonymous function of the form (function () {}()); to get the data from spreadsheet and populate the HTML elements with the values.
Create an HTML file HTML_Demo:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<input id="A8SBwf" type="text">
<input id="gNO89b" type="button" value="Click Me" onclick="buttonOnClick()">
<script>
function buttonOnClick() {
try {
google.script.run.withSuccessHandler(
function(response) {
document.getElementById("A8SBwf").value = response;
}
).getCellA1();
}
catch(err) {
alert(err);
}
}
</script>
</body>
</html>
Then in Code.gs create the getCellA1:
function getCellA1() {
try {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var range = sheet.getRange("A1");
return range.getValue();
}
catch(err) {
return err.message;
}
}
I am trying to implement a workflow - I need to send an email with links to approve/reject a candidate to Level 1 Manager and then to Level 2 Manager. Once both approve, a confirmation email is sent to the candidate.
I have a custom function say main_function() that executes before sending each of the two emails. This function needs to pull data from the spreadsheet to which the script is bound.
Since I have a two-step approval, I created different projects to get separate WebApp URLs for each approval step.
I am including the main_function() function as a library in the two projects.
main_function() sends an email with approve/reject link and when the link is clicked an HTML opens with an input box to take comments.
Then the HTML includes a call to a script function saveToSheets() to save the data to google sheet.
The HTML shows up but data is not getting saved because saveToSheets() is not called. How can I resolve this?
Main function in Library myLib
main_function(){
//do something
Logger.log("function called!");
var htmlTemplate = HtmlService.createTemplateFromFile('Index2.html');
htmlTemplate.ID = ID; //pass variables from script to HTML
htmlTemplate.decision = decision;
htmlTemplate= htmlTemplate.evaluate().setTitle('Comments').setSandboxMode(HtmlService.SandboxMode.NATIVE);
return htmlTemplate.asTemplate();
}
saveToSheets(inputArray){
//do something
}
Index2.html in Library myLib
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
</style>
</head>
<body>
// Includes a COMMENT BOX with id comment1
//include DIV element with id output to catch error
<script>
function runGoogleScript() {
var item0 = "<?= ID ?>";
var item1 = "<?= decision ?>";
var item2 = document.getElementById('comment1').value; //comments
var inputArray =[item0,item1,item2];
google.script.run.withFailureHandler(onFailure).myLib.saveToSheets(inputArray);
}
function onFailure(error) {
var div = document.getElementById('output');
div.innerHTML = "ERROR: " + error.message;
}
function onSuccess() {
}
</script>
</body>
</html>
Function in another project that needs to reuse the script and HTML through mylib
myfunction(){
var htmlTemplate = mylib.main_function();
return htmlTemplate.evaluate();
}
It is incorrect to say "Index.html is not accessible from project". When you deploy a project as a library, its HTML files are in the context of the library.
But if you want to pass an evaluated HTML template from a library as a template you should use asTemplate()
Example:
main_function(){
//do something
Logger.log("function called!");
var htmlTemplate = HtmlService.createTemplateFromFile('Index.html');
htmlTemplate= htmlTemplate.evaluate();
return htmlTemplate.asTemplate();
}
I am wondering how the client-server communication with Google Apps Script Webapps works. In the html page I call my functions with
google.script.run.doSomething();
and I can add a
withFailureHandler(onFailure);
OR
withSuccessHandler(onSuccess) but I can't add both..
So when I want to call a server-side function I normally want to handle the response in the UI ?differently if it is a success than a failure, right? But I want to handle both right? So why do I have to choose between one of them?
Also another problem is that I could not find any information about the actual errors you can have on the server side so that your withFailureHandler(onFailure); can handle them. Can I just do a throw new Error("everything is broken - tell that the user")? If there are permission errors, are they handled? How do I generate an error on the server side so that I can handle it on the client side properly?
It is true that the documentation does not specify it clearly, but you can implement both a success and error handler simultaneously
Sample:
<html>
<head>
<base target="_top">
<script>
google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onFailure).getUnreadEmails();
function onSuccess(numUnread) {
var div = document.getElementById('output');
div.innerHTML = 'You have ' + numUnread + ' unread messages in your Gmail inbox.';
}
function onFailure(error) {
var div = document.getElementById('output');
div.innerHTML = "ERROR: " + error.message;
}
</script>
</head>
<body>
<div id="output"></div>
</body>
</html>
UPDATE
To simulate a failure based on the documentaiton sample, change the working code.gs part
from
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index');
}
function getUnreadEmails() {
return GmailApp.gotInboxUnreadCount();
}
to
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index');
}
function getUnreadEmails() {
return GMailApp.gotInboxUnreadCount();
}
After deploying the WebApp, the html will output:
As written in the official documentation,
You can use any combination and any order of withSuccessHandler(), withFailureHandler(), and withUserObject(). You can also call any of the modifying functions on a script runner that already has a value set. The new value simply overrides the previous value.
Sample:
const gsr = google.script.run;
const runnerF = gsr.withFailureHandler(onFailure);
const runnerS1 = runnerF.withSuccessHandler(onSuccess1);
const runnerS2 = runnerF.withSuccessHandler(onSuccess2);
/*Both runners have the same failure handlers, but different success handlers*/
runnerS1.callServer();//failure => onFailure;success => onSuccess;
runnerS2.callServer2();//failure => onFailure;success => onSuccess2;
How can I print the active user's details in the HTML page? I am able to print in logs and as well as in sheet but not able to print in HTML
<script>
var email = Session.getActiveUser().getEmail();
Logger.log(email);
}
</script>
<p email="email"></p>
Not completely sure what your asking but here's a simple example of getting current user email on to an html dialog. You could deploy it as a web app if you wish.
Code.gs:
function onOpen(e)//for Menu
{
SpreadsheetApp.getUi().createMenu('My Tools')
.addItem('Display Email','displayEmail')
.addToUi();
}
function getCurrentUserEmail()
{
var email={'email':Session.getActiveUser().getEmail()};//returned as an object
return email;
}
function displayEmail()
{
SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutputFromFile('getemailinhtml'), 'User Email');//modeless dialog
}
getemailinhtml.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function() {
google.script.run
.withSuccessHandler(dispEmail)
.getCurrentUserEmail();
});//runs after dom is loaded
function dispEmail(data)
{
$('#email').text(data.email);//put's email into h1 tag
}
console.log('My Code');
</script>
</head>
<body>
<h1 id="email"></h1>//Email show up here
</body>
</html>
To have your script display HTML pages, start with
HTML Service: Create and Serve HTML. A minimal example:
function doGet() {
var email = Session.getActiveUser().getEmail();
var html = '<html><body>Email: ' + email + '</body></html>';
return HtmlService.createHtmlOutput(html);
}
Go to "Publish > Deploy as web app" and set appropriate options, so that the app will run as the user accessing it (who will have to authorize it):
Note that the above example is only suitable as a demo; for larger pages you will want to use templated HTML and other web-oriented features of Google Apps script.
I wrote a google apps script code, it will open a google spread sheet, and list the values by row, but there are 2 problems:
1. The output by random order.
2. The div text which id "loding" change to "Finished!" before list all of values.
I thought the script will wait for server-side function return when I run it by "withSuccessHandler()", but it's not.
How can I correct it?
index.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
function jsListValue() {
// Get count.
google.script.run.withSuccessHandler(function(count) {
// List all values.
for( count; count>0; count=count-1) {
// Get a value.
google.script.run.withSuccessHandler(function(content) {
// Shows in "output".
var new_div = document.createElement("div");
new_div.appendChild(document.createTextNode(content));
document.getElementById("output").appendChild(new_div);
}).gsGetValue(count);
}
// Change loding notice.
document.getElementById("loding").innerHTML = "Finished!";
}).gsGetCount();
}
</script>
</head>
<body onload="jsListValue()">
<div id="output"></div>
<div id="loding">Loding now...</div>
</body>
</html>
code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile('index').setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
function gsOpenSheet() {
// Return sheet of the note data.
return (SpreadsheetApp.openById("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").getSheetByName("sheet1"));
}
function gsGetCount() {
// Return last row index in this sheet.
return (gsOpenSheet().getLastRow());
}
function gsGetValue(index) {
// Return value in the (index,1).
return (gsOpenSheet().getRange(index,1).getValue());
}
GAS is very similar to Javascript, and all calls to Google's server side functions are asynchronous. You cannot change this (at least I haven't seen any doc reg. that).
What you can do, is, use a callback function on the client side which polls the server for a "success" return value. It'll keep polling it say for 1 minute, or else exit. Let it set a client flag to "true" if the success value is returned by the server. Nothing should proceed on the client side, unless the flag is true. In this way, you can control what happens on the client side.
You want to use withSuccessHandler Docs
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
function onSuccess(numUnread) {
var div = document.getElementById('output');
div.innerHTML = 'You have ' + numUnread
+ ' unread messages in your Gmail inbox.';
}
google.script.run.withSuccessHandler(onSuccess)
.getUnreadEmails();
</script>
</head>
<body>
<div id="output"></div>
</body>
</html>
google.script.run is asynchronous, which means that is impossible to predict in what order gsGetValue(count) will return. When you're using withSuccessHandler you must perform the next action inside the callback function.
My suggestion is to get all the range you want and put it on an array. You can create a serverside function to do this. The code would look like this:
//Serverside function
function getDataForSearch() {
const ws = SpreadsheetApp.openById("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").getSheetByName("sheet1");
return ws.getRange(1,1,ws.getLastRow(),1).getValues();
}
getValues() will return an array of arrays that should contain all values from the range specified. More information about here
On your client-side, the script should be like this:
//array to get the data from getRange()
var data;
function jsListValue() {
google.script.run.withSuccessHandler(function(dataReturned){
data = dataReturned;
var new_div;
data.forEach(function(r){
new_div = document.createElement("div");
new_div.appendChild(document.createTextNode(r[0));
document.getElementById("output").appendChild(new_div);
});
document.getElementById("loding").innerHTML = "Finished!";
}).getDataForSearch();
}
As hinted by #Iuri Pereira If you return a parameter from the google script code, and then use it in the page javascript, then the procedure runs synchronously.
HTML:
<button onclick="google.script.run.withSuccessHandler(js_function).gs_code();">action</button>
GS:
function gs_code() {
// do Something;
return true;
}
HTML javascript:
function js_function(gs_return_value) {
console.log(gs_return_value);
}