I'm trying to write code that will help some of my users take a JSON response and convert it to a table in Google Sheets. I have the code for the JSON to Table script, courtesy of Amit Agarwal at www.ctrlq.org.
The way that I'm trying to do is that Google Sheets will generate a pop up for my user to copy and paste the JSON straight into it and then it will pass the object to the JSON to Table code. I'm having a problem figuring out how to actually do that.
Code.gs
function showPrompt()
{
var ui = SpreadsheetApp.getUi(); // Same variations.
var html = HtmlService.createHtmlOutputFromFile('Upload');
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.showModalDialog(html, 'Dialog title');
}
function setJSON(json)
{
var json = json; //json object?
return json;
}
// Written by Amit Agarwal www.ctrlq.org
function writeJSONtoSheet(json) {
var sheet = SpreadsheetApp.getActiveSheet()[1];
var keys = Object.keys(json).sort();
var last = sheet.getLastColumn();
var header = sheet.getRange(1, 1, 1, last).getValues()[0];
var newCols = [];
for (var k = 0; k < keys.length; k++) {
if (header.indexOf(keys[k]) === -1) {
newCols.push(keys[k]);
}
}
if (newCols.length > 0) {
sheet.insertColumnsAfter(last, newCols.length);
sheet.getRange(1, last + 1, 1, newCols.length).setValues([newCols]);
header = header.concat(newCols);
}
var row = [];
for (var h = 0; h < header.length; h++) {
row.push(header[h] in json ? json[header[h]] : "");
}
sheet.appendRow(row);
}
Dialog box HTML:
<!DOCTYPE html>
<html>
<head>
<script>
function setJSON() {
// how do i add value to set to set JSON
var json = ""
google.script.run.setJSON(json);
}
</script>
<base target="_top">
</head>
<body>
text text
<textarea rows="20" cols="20" placeholder="Paste your Data here. Do not format. Do not worry if it looks weird." name="json">DATA</textarea>
<input type="button" class="button" value="Submit JSON" onclick="setJSON()">
</body>
</html>
Thank you in advance!
Here's a simple example of triggering a dialog via an in-sheet menu and passing the input back to a server side function.
Code.gs
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Input Form Test')
.addItem('Input Form', 'showInputForm')
.addToUi();
}
function showInputForm() {
var html = HtmlService.createHtmlOutputFromFile('inputform');
SpreadsheetApp.getUi().showModalDialog(html, 'Input Form Test');
}
function logInput(input) {
Logger.log(input)
}
inputform.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<script>
function setJSON(form) {
var jsonData = document.getElementById('json-data');
google.script.run.logInput(jsonData.value);
google.script.host.close();
}
</script>
<body>
<textarea id="json-data" rows="20" cols="20" placeholder="Paste your data here. Do not format. Do not worry if it looks weird." name="json">DATA</textarea>
<input type="button" class="button" value="Submit JSON" onclick="setJSON()">
</body>
</html>
Logging Output
After clicking the "Submit JSON" button with "test input" entered into the form, this is the output in the script logs:
[18-06-15 13:47:15:741 PDT] test input
Notes
I added an id attribute to the textarea element
the id is used to fetch the element programmatically
you may want to perform some client-side validations in setJSON(). For example:
verify that the value is not still the default value (i.e. DATA)
validate that the JSON data can be parsed succesfully (i.e. JSON.parse())
an example implementation with the above validations:
function setJSON(form) {
var jsonData = document.getElementById('json-data');
if (jsonData.value == 'DATA') {
alert("Please enter JSON data into the form.");
return;
}
try {
JSON.parse(jsonData.value)
}
catch(e) {
console.log(e); // Outputs to the browser console.
alert("Unable to parse JSON data.");
return;
}
google.script.run.logInput(jsonData.value);
google.script.host.close();
}
to fit your use case, in inputform.html you'd replace the logInput() call with either:
a function that calls JSON.parse() and then calls writeJSONtoSheet() with the result
writeJSONtoSheet() if you update it to expect text and add a JSON.parse() call at the beginning of the function
Related
I am working on a Google Sheets macro that displays some text to the user, then presents some buttons for the user to interact with. The buttons run another function and I am struggling with how to have the button display the text to the user.
I can't find the method or object I am supposed to use to grab the currently open window and edit the html to add more text. Or if that isn't possible, how can I close the open window and then display a new window that also has the old text?
function example(text,title,height=90,width=350) {
const dialogbutton = '<br><input type="button" value="Do Stuff" onClick="google.script.run.doStuff();" />'
var html=HtmlService.createHtmlOutput(text+dialogbutton).setHeight(height).setWidth(width)
SpreadsheetApp.getUi().showModelessDialog(html, title);
}
function doStuff() {
const dialogWindow = ???//I am hoping to retrieve the open window as an object
const text = getText() //run some other function and get the new text to insert
dialogWindow.displayedText += text //modify the displayed window to add the new text
}
Here is a very simple example of how to communicate with the server (i.e. Spreadsheet or Doc). In this case a spreadsheet with Sheet1!A1 = hello
Here is a simple dialog
Server side code Code.gs bound to a spreadsheet
function showTest() {
var html = HtmlService.createTemplateFromFile("HTML_Simple");
html = html.evaluate();
SpreadsheetApp.getUi().showModalDialog(html,"Test");
}
function doStuff() {
try {
// let get a value from spreadsheet
let spread = SpreadsheetApp.getActiveSpreadsheet();
let sheet = spread.getSheetByName("Sheet1");
return sheet.getRange("A1").getValue();
}
catch(err) {
Logger.log("Error in doStuff() "+err);
}
}
HTML Page HTML_Simple.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<input type="button" value="Do Stuff" onClick="doStuffOnClick()">
<input type="text" id="whatStuff">
<script>
function doStuffOnClick() {
try {
google.script.run.withSuccessHandler(
function(response) {
document.getElementById("whatStuff").value = response;
}
).doStuff();
}
catch(err) {
alert(err);
}
}
</script>
</body>
</html>
Reference
HTML Service Best Practices
google.script.run()
Trying to make a script to upload a file and paste the downloaded file's link into the active cell of a google spreadsheet.
After clicking "Upload" in the modal window, the file is not written to Google Drive and, accordingly, the link is not written to the cell
Code.gs
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('File')
.addItem('Attach...', 'showForm')
.addToUi();
}
function showForm() {
var html = HtmlService.createHtmlOutputFromFile('index');
SpreadsheetApp.getUi().showModalDialog(html, 'Upload File');
}
function uploadFile(e) {
var newFileName = e.fileName;
var blob = e.file;
var upFile = DriveApp.getFolderById('*FolderID*').createFile(blob).setName(newFileName);
Logger.log(upFile);
var fileUrl = upFile.getUrl();
var formula = '=HYPERLINK("' + fileUrl + '","' + newFileName + '")';
SpreadsheetApp.getActiveRange().setFormula( formula );
return "Uploaded!";
}
index.html
<!DOCTYPE html>
<html>
<head>
<base target="_center">
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
</head>
<body>
<form id="myForm" >
Select File: <input type="file" name="file" accept="*" /><br>
File name: <input type="text" name="fileName" /><br><br>
<input type="button" value="Upload" onclick="upload(this.parentNode);" />
</form>
<script>
window.onload=func1;
function func1() {
document.getElementById('myForm').addEventListener('submit', function(event) {
event.preventDefault();
});
}
function upload(obj){
google.script.run.withSuccessHandler(close).withFailureHandler(close).uploadFile(obj);
}
function close(e) {
console.log(e);
google.script.host.close();
}
</script>
</body>
</html>
Issue and workaround:
On December 9, 2021, the file object got to be able to be parsed from the Javascript side to the Google Apps Script side with V8 runtime. But, in this case, this can be used for only Web Apps. In the current stage, the sidebar and dialog cannot parse the file object on the Javascript side. Ref I think that this is the reason of your issue. So, in the current stage, it is required to send the file object as the string and the byte array for the sidebar and the dialog.
In this case, in order to achieve your goal using the current workaround, I would like to propose the following 2 patterns.
By the way, I think that in your Google Apps Script, an error occurs at SpreadsheetApp.getActiveCell().setFormula( formula );. Because SpreadsheetApp has no method of getActiveCell(). In this case, I think that getActiveRange() might be suitable.
Modified script:
In this modification, the file object is converted to the byte array, and the data is sent to Google Apps Script side.
Google Apps Script side:
function showForm() {
var html = HtmlService.createHtmlOutputFromFile('index');
SpreadsheetApp.getUi().showModalDialog(html, 'Upload File');
}
function uploadFile(e) {
var blob = Utilities.newBlob(...e);
var upFile = DriveApp.getFolderById('*FolderID*').createFile(blob).setName(e[2]);
Logger.log(upFile);
var fileUrl = upFile.getUrl();
var formula = '=HYPERLINK("' + fileUrl + '","' + e[2] + '")';
SpreadsheetApp.getActiveRange().setFormula(formula);
return "Uploaded!";
}
HTML & Javascript side:
<!DOCTYPE html>
<html>
<head>
<base target="_center">
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
</head>
<body>
<form id="myForm">
Select File: <input type="file" name="file" accept="*" /><br>
File name: <input type="text" name="fileName" /><br><br>
<input type="button" value="Upload" onclick="upload(this.parentNode);" />
</form>
<script>
window.onload = func1;
function func1() {
document.getElementById('myForm').addEventListener('submit', function(event) {
event.preventDefault();
});
}
function upload(obj) {
const file = obj.file.files[0];
// --- For your additional request, I modified below script.
const extension = function(e) {
const temp = e.split(".");
return temp.length == 1 ? "" : temp.pop();
}(file.name);
const filename = `${obj.fileName.value}.${extension}`;
// ---
const fr = new FileReader();
fr.onload = e => {
const obj = [[...new Int8Array(e.target.result)], file.type, filename];
google.script.run.withSuccessHandler(close).withFailureHandler(close).uploadFile(obj);
};
fr.readAsArrayBuffer(file);
}
function close(e) {
console.log(e);
google.script.host.close();
}
</script>
</body>
</html>
Note:
In your script, I thought that the modified script might be a bit complicated. So, I posted the modified script as an answer.
Reference:
Related thread.
How do I convert jpg or png image to Blob via a Google Apps Script Web App?
i have created a google web app,
when i am typing a number in the orders textbox,
i'm getting a match result in the disabled amount textbox,
from the google sheet column,
the web app working good
now,what i want to do is to be able to create a textbox in app inventor
and when put an orders value and clicking a button
i will get the amount result on a label on the app invenotr interface
its not working on app inventor
here is the code i made in app inventor
[![enter image description here][1]][1]
this the Code.gs
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('page');
}
function getCost(oneCode){
var url = "https://script.google.com/macros/s/AKfycbyX";
va
return "not found";
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<script>
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("one").addEventListener("change", doThis);
});
function doThis() {
var oneCode = document.getElementById("one").value;
google.script.run.withSuccessHandler(updateAmount).getCost(oneCode);
}
function updateAmount(cost) {
document.getElementById("two").value = cost;
}
</script>
<body>
<div>
<input id="one" type="text">
<label for="one">orders</label>
</div>
<div>
<input disabled id="two" type="text">
<label for="two">amount</label>
</div>
</body>
</html>
Try this:
function doThis() {
var oneCode = document.getElementById("one").value;
console.log(oneCode);
google.script.run.withSuccessHandler(updateAmount).getCost(oneCode);
}
and this:
function getCost(oneCode){
console.log('onecode');
var url = "https://script.google.com/macros/s/AKfycbyXX6axO9G9ANDW0LaQVgezT6hSzSld8CRt2VbsZV6v8XXwNus/exec";
var ss = SpreadsheetApp.openByUrl(url);
var ws = ss.getSheetByName("Sheet1");
var data = ws.getRange(1,1,ws.getLastRow(),2).getValues();
var ordersList = data.map(function(r){ return r[0].toString(); });
var amountList = data.map(function(r){ return r[1].toString(); });
var position = ordersList.indexOf(oneCode);
if(position > -1){
return amountList[position];
}
return "not found";
}
And then run the code and goto View/Executions and see where you losing the data.
The Google support article example under the Forms heading is broken. From the article:
If you call a server function with a form element as a parameter, the form becomes a single object with field names as keys and field values as values. The values are all converted to strings, except for the contents of file-input fields, which become Blob objects.
I tested this by passing a Form element containing 5 text inputs and a file, then logging Object.keys() on the form object. It returned only the 5 text fields, the file was stripped from the form object. Attempting to assign the file blob directly returned Exception: Invalid argument: blob
How do I pass the file blob from the client-side Form to the server-side Apps Script?
EDIT: To clarify, I also copy-pasted the example provided by Google verbatim. It errors with Exception: Invalid argument: blob.
To reproduce:
Create new Google Apps Script project
Index.html contents:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
// Prevent forms from submitting.
function preventFormSubmit() {
var forms = document.querySelectorAll('form');
for (var i = 0; i < forms.length; i++) {
forms[i].addEventListener('submit', function(event) {
event.preventDefault();
});
}
}
window.addEventListener('load', preventFormSubmit);
function handleFormSubmit(formObject) {
google.script.run.withSuccessHandler(updateUrl).processForm(formObject);
}
function updateUrl(url) {
var div = document.getElementById('output');
div.innerHTML = 'Got it!';
}
</script>
</head>
<body>
<form id="myForm" onsubmit="handleFormSubmit(this)">
<input name="myFile" type="file" />
<input type="submit" value="Submit" />
</form>
<div id="output"></div>
</body>
</html>
Code.gs contents:
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index');
}
function processForm(formObject) {
var formBlob = formObject.myFile;
var driveFile = DriveApp.createFile(formBlob);
return driveFile.getUrl();
}
Publish as Web App
Submit the form with any file
Observe error in View -> Stackdriver Logging -> Apps Script Dashboard
Here's an example:
html:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
function fileUploadJs(frmData) {
document.getElementById('status').style.display ='inline';
google.script.run
.withSuccessHandler(updateOutput)
.uploadTheFile(frmData)
}
function updateOutput(info) {
var br='<br />';
var outputDiv = document.getElementById('status');
outputDiv.innerHTML = br + 'File Upload Successful.' + br + 'File Name: ' + info.name + br + 'Content Type: ' + info.type + br + 'Folder Name: ' + info.folder;
}
console.log('My Code');
</script>
<style>
body {background-color:#ffffff;}
input{padding:2px;margin:2px;}
</style>
</head>
<body>
<h1 id="main-heading">Walking Tracks</h1>
<h3>Upload GPS Tracks Files</h3>
<div id="formDiv">
<form id="myForm">
<input name="fileToLoad" type="file" /><br/>
<input type="button" value="Submit" onclick="fileUploadJs(this.parentNode)" />
</form>
</div>
<div id="status" style="display: none">
<!-- div will be filled with innerHTML after form submission. -->
Uploading. Please wait...
</div>
<div id="controls">
<input type="button" value="Close" onClick="google.script.host.close();" />
</div>
</body>
</html>
server code:
function uploadTheFile(theForm) {
var fileBlob=theForm.fileToLoad;
var fldr = DriveApp.getFolderById('FolderId');
var file=fldr.createFile(fileBlob);
var fi=formatFileName(file);
var fileInfo={'name':fi.getName(),'type':fileBlob.getContentType(), 'size':fileBlob.getBytes(), 'folder':fldr.getName()};
return fileInfo;
}
I can confirm that this doesn't work in G-Suite Enterprise. I don't know why because I cannot find documentation that says how Google is serializing the data. It could be a browser/computer security setting or something in G-Suite.
However, there is an easier way to accomplish your need. You can use a Google Form with a file upload question and then create an on form submit trigger/event on it to copy the file to a team/shared drive. Here is sample code if you want to attach the trigger to the Google Form itself:
// ID of the destnation folder to save the file in
var destinationFolderID = "10gkU_2V9iYy-VKudOCOjydEpoepPTgPv"
function saveFileToTeamDrive(e)
{
// a place to save the URL of the uploaded file
var fileID;
// go through all of the responses to find the URL of the uploaded file
e.response.getItemResponses().forEach(function(itemResponse){
// once we find the question with the file
if(itemResponse.getItem().getTitle() == "File Upload Test")
{
// get the file ID from the response
fileID = itemResponse.getResponse();
return;
}
});
// stop if we didn't have one
if(!fileID.length) return;
// get the first index in the array
fileID = fileID[0];
// get the file
var file = DriveApp.getFileById(fileID);
// get the destination folder
var destinationFolder = DriveApp.getFolderById(destinationFolderID);
// make a copy
var newFile = file.makeCopy(destinationFolder);
Logger.log(newFile.getUrl());
}
You can also attach to the on form submit event of a Google Sheet that is linked to a Google Form. I find that way easier cause the Google Sheet on form submit trigger/event includes a JSON of the question/answers so you don't have to iterate all of them to find it. It also means you can re-run a submission if it failed.
WARNING
One important note, if you do either of these things do not give anyone else edit access to the code. This is because as soon as you create and authorize the trigger, anyone who has edit access to the code would be able to use it to gain access to your Google Drive (and anything else the trigger is authorized for). Please see securing a Google Apps Script linked to an authorized trigger so others can edit for more information.
Background
I'm fairly new to coding in general and as I've gone through building this little UI for Google Sheets using GAS, this is one of the concepts that has just has me stumped. I've tried and tried reading, understand conceptually and apply withSuccessHandler(function) to all sorts of examples but I just can't seem to get it to work.
Here's what I understand so far:
When you run the following on the client side (e.g. Index.html):
google.script.run.withFailureHandler(CallbackFunction).ServerSideFunction(aVariable)
a. ServerSideFunction(aVariable): This function from your Code.gs is first called and returns a value, "OutputA," back to Index.html.
b. CallbackFunction: Then, this function is called and uses "OutputA" as its input and returns another value which you can use for whatever purpose.
Goal
Open Dialog Box asking for "Name" and "E-mail." DONE
Once user hits "Submit" the input is populated in the spreadsheet. DONE
The form is then cleared so they can potentially add another person or exit if they want.
Question
For that last piece, I can't seem to get withSuccessHandler to return a value back from my script, confirming that the inputs were entered properly, and I really don't know how to proceed.
I've included a working version of the code below without withSuccessHandler for reference. Any help at all in better understanding this and how I can incorporate it into the code below would be greatly appreciated!
Google Script
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu('Menu')
.addItem('Add Member', 'createDialog')
.addToUi();
}
function createDialog() {
var htmlOutput = HtmlService
.createHtmlOutputFromFile('Index')
.setWidth(300)
.setHeight(300);
SpreadsheetApp.getUi().showModalDialog(htmlOutput, " ");
}
function addAName(bName, bEmail) {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var ControlSheet = sheet.getSheetByName('Control Sheet');
var lRow = ControlSheet.getRange(1, 1, ControlSheet.getLastRow(), 1).getValues().filter(String).length;
var Name = ControlSheet.getRange(lRow + 1, 1);
var Email = ControlSheet.getRange(lRow + 1, 2);
var ValidName = /(^[A-Za-z]+)\s([A-Za-z]{1}[.]{1}\s)?([A-Za-z]+$)/g;
var ValidEmail = /(^[A-Za-z]+)([.]{1})([A-Za-z]{1}[.]{1})?([A-Za-z]+)(#google.com|#yahoo.com)$/g;
if (bName.match(ValidName) && bEmail.match(ValidEmail)) {
Name.setValue(bName);
Email.setValue(bEmail);
}
}
HTML
<body>
<h2>Add New Member</h2>
<div class="mdc-layout-grid">
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-12">
<div class="mdc-text-field mdc-text-field--upgraded" data-mdc-auto-init="MDCTextField">
<input class="mdc-text-field__input" id="Name" type="text" aria-controls="name-validation-message" pattern="(^[A-Za-z]+)\s([A-Za-z]{1}[.]{1}\s)?([A-Za-z]+$)">
<label for="Name" class="mdc-floating-label">Name</label>
<div class="mdc-line-ripple"></div>
</div>
<p class="mdc-text-field-helper-text mdc-text-field-helper-text--validation-msg" id="name-validation-message" aria-hidden="true">
Please enter your full name.
</p>
</div>
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-12">
<div class="mdc-text-field" data-mdc-auto-init="MDCTextField">
<input class="mdc-text-field__input" id="Email" type="text" aria-controls="name-validation-message" pattern="(^[A-Za-z]+)([.]{1})([A-Za-z]{1}[.]{1})?([A-Za-z]+)(#google.com|#yahoo.com)$">
<label for="Email" class="mdc-floating-label">Email Address</label>
<div class="mdc-line-ripple"></div>
</div>
<p class="mdc-text-field-helper-text mdc-text-field-helper-text--validation-msg" id="name-validation-message" aria-hidden="true">
Please enter a valid Google or Yahoo email address.
</p>
</div>
</div>
<div class="Right_Side">
<button class="mdc-button mdc-button--unelevated secondary-filled-button" OnClick="google.script.host.close()">Cancel
</button>
<button class="mdc-button mdc-button--unelevated primary-filled-button" OnClick="sendInputToGS()">Submit
</button>
</div>
<script>
window.sendInputToGS = function() {
var aName = document.getElementById("Name").value;
var aEmail = document.getElementById("Email").value;
google.script.run.addAName(aName, aEmail);
}
</script>
<script type="text/javascript">
window.mdc.autoInit();
</script>
</body>
Only one parameter can be added to the server function being called. But you can use an array as the one parameter and put multiple values into the array.
var data_Array = [aName, aEmail];
google.script.run
.withSuccessHandler(myClientSideFunction)
.addAName(data_Array);
A value is not automatically returned, you must add a return myValue statement.
function addAName(data) {
var bName, bEmail;
bName = data[0];//Arrays in JavaScript are zero indexed
bEmail = data[1];
//code
return "something";
}
Client Code
<script>
window.sendInputToGS = function() {
var aName = document.getElementById("Name").value;
var aEmail = document.getElementById("Email").value;
var data_Array = [aName, aEmail];
google.script.run
.withSuccessHandler(confirmationBack)
.addAName(data_Array);
}
window.confirmationBack = function(rtrn) {
if (rtrn === 'success') {
}
}
</script>
The success and failure handlers for the async communication between the Apps Script instance and the client-side HTML code are functions in the client-side code, not written in Apps Script (.gs).
When calling your server-side function foo(input), it can be used to call other server-side functions (bar(), baz()) as desired, and use their outputs to form the value that is sent to the client-side success handler.
Example:
.gs
function foo(valFromClient) {
var firstPart = bar(valFromClient);
var secondPart = baz(valFromClient);
return {input: valFromClient, first: firstPart, second: secondPart};
}
function bar(input) {
return "'bar' called, input '" + String(input) + "'.";
}
function baz(val) {
return "'baz' called, input '" + String(val) + "'.";
}
.html
...
<script>
function getServerStuff() {
google.script.run
.withFailureHandler(serverThrewException)
.withSuccessHandler(serverFunctionCalledReturn)
.foo("Hi");
}
function serverThrewException(err) {
console.log(err);
}
function serverFunctionCalledReturn(value) {
console.log(value);
}
</script>
In this example, because our code does not throw any exceptions, the failure handler will never be called, and only the success handler will be called. The browser console would log the object {input: "Hi", first: "'bar' called, input 'Hi'.", second: "'baz' called, input 'Hi'."}.