AppScript: How to create an upload form? - google-apps-script

I am developing an Add-on for Gmail using AppScript.
My objective is to create something similar to the image below. Any hints?

Problem
File upload in Gmail Add-ons. In short - not exactly. Gmail Add-ons use CardService class to build the Ui - and it doesn't have a file input type, nor any drag-and-drop functionality. But there is a workaround.
Step 1. Create trigger widget
Then, ensure that your Card contains a CardSection with an ImageButton, TextButton or KeyValue widget (KeyValue is deprecated, use DecoratedText) that has an OpenLink action set on them. When using the setUrl(url) method to setup URL to open on widget click, use the current project's URL (when deploying both as WebApp and Add-on) that can be accessed dynamically via ScriptApp.getService().getUrl() call.
Step 2. Create file submit form
In the Add-on project, create an Html file that will handle the file upload. You can use sample one or create your own implementation. the sample file uses FileReader Web API to handle the submitted file (note that client-to-server communication in Google Apps Script requires preventing submit event handler and calling a server-side function via goolge.script.run API only).
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<form>
<input name="file" type="file" />
<button id="submit" type="submit">Save file</button>
</form>
<script>
var form = document.forms[0];
form.addEventListener('submit', (event) => {
event.preventDefault();
var file = form.elements[0].files[0];
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = () => {
var buffer = reader.result;
var data = Array.from(new Int8Array(buffer));
google.script.run.withSuccessHandler((server) => {
top.window.close();
}).saveFile(data,file.name,file.type);
};
});
</script>
</body>
</html>
Step 3. Setup doGet()
In your WebApp code, add the required doGet() function that will show the file upload form that we created during step 2. It can be as simple as a couple lines of code (just make sure to return the html file parsed by HtmlService):
function doGet() {
var html = HtmlService.createHtmlOutputFromFile('file name from step 2');
return html;
}
Step 4. Handle file upload
In your WebApp code, add handler that will receive file data (this sample assumes you read it as byte[], see step 2 for details).
function saveFile(upload,name,mime) {
var blob = Utilities.newBlob(upload,mime,name);
var file = DriveApp.createFile(blob);
Logger.log(file.getUrl()) //test upload;
//handle file as needed;
return;
}
Step 5. Deploy as WebApp
Lastly, you will have to deploy your Add-on as both WebApp (or bundle with one) and an Add-on. Assuming you've already configured manifest for the Add-on, go to "Publish" menu, select "Deploy as web app", create a deployment and allow access to anyone.
Notes
This method won't allow you to easily update the Ui to show which files were uploaded, but you can add a withSuccessHandler() call to google.script.run that on successful server-side handling of the uploaded file closes the window with the form, save state info to cache / user properties. Then, if you set the OpenLink's OnClose property to RELOAD_ADD_ON (see step 1), you will be able to conditionally update Ui to notify the user of successful upload.
UPDATE: after Tanaike's comment I reworked the upload process to better handle files: changed binary string file read to ArrayBuffer transformed to Int8Array and uploaded as an Array instance.
Current issue is the .g* files upload (despite correct transfer). Will update the answer when solved.
References
OpenLink class reference;
FileReader Web API reference on MDN;
newBlob() method reference (Utilities class);
Client-to-server communication in GAS guide;
Creating and serving HTML in GAS guide;
Web Apps guide;

Related

Google sheets modal doesn't submit form but as a standalone webapp it's working

I have a custom UI function, that opens a modal dialog in my Google Sheets file. The modal is a simple HTML with a form, that allows the user to choose a file from local hard drive, to upload it into Google Drive. The HTML part for this form is as follows:
<form>
<input type="file" name="theFile" id="file-check" accept="image/*">
<input type="button" class="btn btn-info" value="Add photo" id="add-image">
</form>
And the code for sending the data is as follows:
$("#add-image").click(function(){
var val = $('#file-check').val();
if (val == ''){alert('Choose a file to upload');return;}
$('#add-image').attr("disabled", true);
$('#add-image').attr("value", "Adding... please wait.");
google.script.run.withSuccessHandler(refreshIt).withFailureHandler(show_error).uploadImage(this.parentNode);
});
But when I submit the form, the response from the server is this:
{theFile=null}
I've tried to set up this modal dialog as a standalone web-app and it's working normally...
The response from the server from the standalone webapp is as follows:
{theFile=FileUpload}
And the file is being uploaded without any problems!
Why is it working normally as a standalone webapp but sending the null as a modal dialog?
Issue and workaround:
Why is it working normally as a standalone webapp but sending the null as a modal dialog?
I thought that the reason for this issue is due to the current specification. After V8 runtime was released, there was a bug that when the file is sent from HTML form to Google Apps Script side using google.script.run, the file blob was the invalid data. But, on Nov 26, 2021, this bug could be removed. Ref But, when I tested this, it was found that this bug could be removed for Web Apps, and this bug cannot be removed for the dialog and sidebar. Ref This has already been reported to the issue tracker. I believe that this bug will be resolved in the future updated.
In the current stage, in order to upload a file with the dialog and sidebar, it is required to use a workaround. In this answer, I would like to propose a workaround. When your script is modified, it becomes as follows.
Modified script:
From:
$("#add-image").click(function(){
var val = $('#file-check').val();
if (val == ''){alert('Choose a file to upload');return;}
$('#add-image').attr("disabled", true);
$('#add-image').attr("value", "Adding... please wait.");
google.script.run.withSuccessHandler(refreshIt).withFailureHandler(show_error).uploadImage(this.parentNode);
});
To:
$("#add-image").click(function(){
var val = $('#file-check').val();
if (val == ''){alert('Choose a file to upload');return;}
$('#add-image').attr("disabled", true);
$('#add-image').attr("value", "Adding... please wait.");
const file = this.parentNode.theFile.files[0];
const fr = new FileReader();
fr.onload = function(e) {
google.script.run.withSuccessHandler(refreshIt).withFailureHandler(show_error).uploadImage([[...new Int8Array(e.target.result)], file.type, file.name]);
};
fr.readAsArrayBuffer(file);
});
In this case, the function uploadImage of Google Apps Script can be modified as follows. Unfortunately, I cannot see your current script of uploadImage. So please modify your script using the following sample.
function uploadImage(obj) {
const blob = Utilities.newBlob(...obj);
DriveApp.createFile(blob);
return "done"; // Please set your expected return value.
}
References:
HTML form file-input fields not in blob compatible format
[Fixed] Google Apps Script Web App HTML form file-input fields not in blob compatible format

FormResponse object missing when using DriveApp and Smartsheets Sync

My ultimate goal is to access the contents of a file uploaded via a Google Form from within a function triggered by formSubmit. I added some info in a comment to this question, but I think I need to update the question itself. When I deactivate the Smartsheets Sync add-on in the web form, this all works as expected. My theory is that the Smartsheets Sync add-on is not preserving the Event object in certain scenarios.
I began with:
function onFormSubmit (e) {
Logger.log (e);
}
I set up my trigger and tested a form submission, and in the log, I saw:
[<datetime>] {authMode=FULL, source=Form, response=FormResponse triggerUid=<id>}
as expected. I also explored the FormResponse object and verified that a valid Google Drive ID is in the response.
Next, I added a call to DriveApp.getFileById:
function onFormSubmit (e) {
Logger.log (e);
var responses = e.response.getItemResponses ();
var file = DriveApp.getFileById (responses [1].getResponse ());
Logger.log (file);
}
Resubmitting a form brings up a permission error with DriveApp. Not surprising, so I ran onFormSubmit directly from the script editor. It failed because it was invoked without an Event object, but it did invoke the dialog that allowed me to grant DriveApp permissions.
Now, when I submit a form, the Event object doesn't contain a FormResponse object. From the log:
[<datetime>] {authMode=FULL, source=Form, triggerUid=<id>}
So, does granting DriveApp permission somehow revoke permission to inspect the user's response? Alternatively, is there another way for me to use Google App Script to access a file uploaded via a Google Form?
The file ID is put into the file upload answer (response). The following code gets the answer to the file upload question, which is the file ID. Note that arrays are zero indexed, so the first question is at index zero. This code assumes that the file upload question is the very first question.
If this answers your question, you can mark it as correct by clicking the green arrow.
function onFormSubmit(e) {
var file,fileID,form,responseID,submittedResponse,uploadResponse;
responseID = e.response.getId();//The the ID of the current reponse
Logger.log('responseID: ' + responseID)
form = FormApp.getActiveForm();//Get the Form that this script is bound to
submittedResponse = form.getResponse(responseID);//Get the response that
//was just submitted
uploadResponse = submittedResponse.getItemResponses()[0];//This assumes
//that the very first question is the file upload
fileID = uploadResponse.getResponse();//Get the file ID of the file just uploaded
Logger.log('fileID: ' + fileID)
file = DriveApp.getFileById(fileID);//Get the file
Logger.log('file.getName(): ' + file.getName());//verify that this is
//the correct file - and that the code is working
}

How to download / upload the JSON representation of a Google doc?

Is it possible to download, modify, and upload the JSON representation of a Google doc via an API?
I'm trying to write a server side app to do this. By Google doc, I mean files underlying the rich-text editing features as per https://docs.google.com.
As far as I've understood, the RealTime API should allow me to download the json representation of a doc with a GET request, and upload a new JSON file with a PUT request. From the documentation it sounds ideal. However, responses from GET requests contain null in the data field. I understand that this is because my OAuth2.0 app is not the same app that created the document. I'm not sure if/how I could fix this if I want the files to be treated the same as any other Google doc (as defined above).
The Drive API allows me to download a file with a GET request, however, the supported mime-types do not include JSON. I am aware that I could try and convert them (e.g. via a library like the excellent pandoc) but this require lossy and unpredictable processing to try to guess at what Google's document representation might be via e.g. parsing MS Word documents (ew).
Is there a way to directly import & export docs in Google's own JSON representation?
You may want to try using the Realtime API in an unauthenticated mode, called in-memory mode which allows you to get started with the API without any configuration or login.
To build An Unauthenticated App, you may visit and try the steps given in Google Realtime API Quickstart. You can simply copy the following code into a new file and then open it in a browser.
<!DOCTYPE html>
<html>
<head>
<title>Google Realtime Quickstart</title>
<!-- Load Styles -->
<link href="https://www.gstatic.com/realtime/quickstart-styles.css" rel="stylesheet" type="text/css"/>
<!-- Load the Realtime API JavaScript library -->
<script src="https://apis.google.com/js/api.js"></script>
</head>
<body>
<main>
<h1>Realtime Collaboration Quickstart</h1>
<p>Welcome to the quickstart in-memory app!</p>
<textarea id="text_area_1"></textarea>
<textarea id="text_area_2"></textarea>
<p>This document only exists in memory, so it doesn't have real-time collaboration enabled. However, you can persist it to your own disk using the model.toJson() function and load it using the model.loadFromJson() function. This enables your users without Google accounts to use your application.</p>
<textarea id="json_textarea"></textarea>
<button id="json_button" class="visible">GetJson</button>
</main>
<script>
// Load the Realtime API, no auth needed.
window.gapi.load('auth:client,drive-realtime,drive-share', start);
function start() {
var doc = gapi.drive.realtime.newInMemoryDocument();
var model = doc.getModel();
var collaborativeString = model.createString();
collaborativeString.setText('Welcome to the Quickstart App!');
model.getRoot().set('demo_string', collaborativeString);
wireTextBoxes(collaborativeString);
document.getElementById('json_button').addEventListener('click', function(){
document.getElementById('json_textarea').value = model.toJson();
});
}
// Connects the text boxes to the collaborative string.
function wireTextBoxes(collaborativeString) {
var textArea1 = document.getElementById('text_area_1');
var textArea2 = document.getElementById('text_area_2');
gapi.drive.realtime.databinding.bindString(collaborativeString, textArea1);
gapi.drive.realtime.databinding.bindString(collaborativeString, textArea2);
}
</script>
</body>
</html>
Hope that helps!

How to programmatically read-write scripts for offline usage in chrome extension?

I need to have predefined scripts, accessible from chrome content_script, that could be updated automatically from given URL.
Exactly what i do:
I have content_script.js. Inside it, i`d like to create iframe for current page from predefined html+css+js.Sometimes html or css or js can be changed. I want to avoid updating extension, instead, each time user have internet, he could load fresh html+css+js for further offline usage.
So, how to read and write some internal files within extension from content script (or delegate this task to background script)?
You can use HTML5 Filesystem to have a read/write place for files, or just store it as strings in chrome.storage (with "unlimitedStorage" permission as needed) for later reuse.
This code can then be executed in a content script using executeScript, or, if you enable 'unsafe-eval' for the extension CSP, in the main script (which is dangerous, and should be avoided in most cases).
Note that this Filesystem API has a warning that's it's only supported in Chrome, but that shouldn't be a problem (Firefox / WebExtensions platform explicitly reject self-update mechanisms).
You can do read extension file contents, but you can't write to extension folder since it is sandboxed.
To read an extension file, you can just send Ajax call using chrome.runtime.getURL("filepath") as url
var xhr = new XMLHttpRequest();
xhr.open('GET', chrome.runtime.getURL('your file path'), true);
xhr.onreadystatechange = function() {
if (chr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
var text = xhr.responseText;
// Do what you want using text
}
};
xhr.send();

Google apps script won't update app

I am brand new to this and I know this is probably something simple but I just can't seem to get this working. I found this app script online that lets people upload files to my Google Drive but when I try to change anything in it and save it doesn't reflect in the app. I try to add CSS or update the text in Google Script editor then press Save then Run doGet function but nothing pops up to show me the page and the app doesn't change at all. It is deployed and authorized already.
server.gs:
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('form.html');
}
function uploadFiles(form) {
try {
var dropbox = "Student Files";
var folder, folders = DriveApp.getFoldersByName(dropbox);
if (folders.hasNext()) {
folder = folders.next();
} else {
folder = DriveApp.createFolder(dropbox);
}
var blob = form.myFile;
var file = folder.createFile(blob);
file.setDescription("Uploaded by " + form.myName);
return "File uploaded successfully " + file.getUrl();
} catch (error) {
return error.toString();
}
}
form.html:
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
<?!= include('style'); ?>
<form id="myForm">
<input type="text" name="myName" placeholder="Your name..">
<input type="file" name="myFile">
<input type="submit" value="Upload File"
onclick="this.value='Uploading..';
google.script.run.withSuccessHandler(fileUploaded)
.uploadFiles(this.parentNode);
return false;">
</form>
<div id="output"></div>
<script>
function fileUploaded(status) {
document.getElementById('myForm').style.display = 'none';
document.getElementById('output').innerHTML = status;
}
</script>
Thanks to #billy98111
Each time you deploy your web app it uses the same version. That's why it does not update.
Once you made changes, create new version then deploy it. Then when you deploy choose the the version you want to run
If you are using the URL with 'exec' on the end, that URL only serves the last version deployed, it won't show you the development version. Secondly, in order for any client side HTML, JavaScript, CSS to be displayed, you must refresh the browser tab. You don't need to refresh the browser tab to see results of any changes in .gs server side code. I have experienced situations where it seems like I needed to reload the browser, or reboot my computer to get something to work, but I can't verify that, and it could have just been a stupid mistake on my part. If it turns out to be none of those issues, let me know. I highly doubt it has anything to do with your code itself. If it's a shared file, and you are not the owner, there can be situations where the owner didn't authorize a new permission, so new code that needs a different permission that had not been authorized before won't run.
I believe you haven't published your app yet!
You need to go to Publish, Deploy as Web App.
There you'll have to generate a new version and them you can Deploy it, after that, you'll get a window showing the Deployed URL (the official URL) and a test URL (in order to test each save you make on your code):
After that if you need to publish new modifications officially, you have to go to File, manage Versions and create a new Version, after that you choose this new version on the Deploy menu again.