Google Drive Picker - Not showing all folders - google-apps-script

I am using the Google Drive Picker API to facilitate file upload from within a Google Spreadsheet.
The problem the user is reporting is that not all folders in a sub folder are visible or searchable.
The sub folder in question has a large amount of sub folders itself. (About a 1000+)
Does anyone know if Drive Picker has some kind of a limitation around the number of folders it can display?
As requested my code below:
function showPicker() {
var html = HtmlService.createHtmlOutputFromFile('0.2 Picker.html')
.setWidth(600)
.setHeight(425)
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
SpreadsheetApp.getUi().showModalDialog(html, 'Select Invoice(s)');
}
function getOAuthToken() {
DriveApp.getRootFolder();
return ScriptApp.getOAuthToken();
}
//Picker.html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<script type="text/javascript">
var DIALOG_DIMENSIONS = {
width: 600,
height: 425
};
var pickerApiLoaded = false;
function onApiLoad() {
gapi.load('picker', {
'callback': function() {
pickerApiLoaded = true;
}
});
google.script.run.withSuccessHandler(createPicker)
.withFailureHandler(showError).getOAuthToken();
}
function createPicker(token) {
if (pickerApiLoaded && token) {
var docsView = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
.setIncludeFolders(true)
.setMimeTypes('application/vnd.google-apps.spreadsheet')
.setParent("ID_OF_FOLDER_GOES_HERE");
var picker = new google.picker.PickerBuilder()
.addView(docsView)
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.enableFeature(google.picker.Feature.SUPPORT_DRIVES)
.hideTitleBar()
.setSize(DIALOG_DIMENSIONS.width - 2, DIALOG_DIMENSIONS.height - 2)
.setOAuthToken(token)
.setCallback(pickerCallback)
.setOrigin('https://docs.google.com')
.build();
picker.setVisible(true);
} else {
showError('Unable to load the file picker.');
}
}
/**
* A callback function that extracts the chosen document's metadata from the
* response object. For details on the response object, see
* https://developers.google.com/picker/docs/result
*
* #param {object} data The response object.
*/
function pickerCallback(data) {
//Get the user's response action
var action = data[google.picker.Response.ACTION];
//Test if users selected "Picked", if true, do the following:
if (action == google.picker.Action.PICKED) {
//Get documents uploaded by google picker
var files = data[google.picker.Response.DOCUMENTS];
//Create an array to house ID's of uploaded documents
var arrayOfIds = [];
//For the number of elements in the files array do the following
for (var i = 0; i < files.length; i++) {
//Get the id of the current file
var id = files[i][google.picker.Document.ID]
//Push id of current file into arrayOfIds
arrayOfIds.push(id)
}//END OF FOR LOOP I
//Call getInvoiceData passing in array of IDs
google.script.run.getInvoiceData(arrayOfIds);
//Close the upload box
google.script.host.close()
}//END OF IF STATEMENT
//Check action does not "Picked", check if action is "Cancel"
else if (action == google.picker.Action.CANCEL) {
google.script.host.close();
}//END OF ELSE IF
}//END OF FUNCTION
function showError(message) {
document.getElementById('result').innerHTML = 'Error: ' + message;
}
</script>
</head>
<body>
<div>
<p id='result'></p>
</div>
<script type="text/javascript" src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
</body>
</html>

Related

Can I read a drive file opened using Google Picker with drive.file oauth scope?

I created a forms addon with scope:
https://www.googleapis.com/auth/drive.file
and created a picker:
function onOpen(e)
{
FormApp.getUi().createAddonMenu()
.addItem('Sync to Drive', 'showPicker')
.addToUi();
}
function getOAuthToken() {
return ScriptApp.getOAuthToken();
}
function showPicker() {
var html = HtmlService.createHtmlOutputFromFile('Picker.html')
.setWidth(600)
.setHeight(425)
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
FormApp.getUi().showModalDialog(html, 'Select Folder');
}
// Use Advanced Drive Service - https://developers.google.com/apps-script/advanced/drive
function getFileWithAdvancedDriveService(fileId)
{
var drivefl = Drive.Files.get(fileId);
FormApp.getUi().alert('File: '+drivefl);
return drivefl;
}
// Use Drive Service - https://developers.google.com/apps-script/reference/drive
function getFileWithDriveService(fileId)
{
var drivefl = DriveApp.getFileById(fileId);
FormApp.getUi().alert('File: '+drivefl);
return drivefl;
}
After I open a file using this picker, I am trying to read it in the pickerCallback:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css" />
<script type="text/javascript">
var DIALOG_DIMENSIONS = {
width: 600,
height: 425,
};
var authToken;
var pickerApiLoaded = false;
function onApiLoad() {
gapi.load('picker', {
callback: function () {
pickerApiLoaded = true;
},
});
google.script.run.withSuccessHandler(createPicker).withFailureHandler(showError).getOAuthToken();
}
function createPicker(token) {
authToken = token;
if (pickerApiLoaded && authToken) {
var docsView = new google.picker.DocsView()
.setIncludeFolders(true);
var picker = new google.picker.PickerBuilder()
.addView(docsView)
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.hideTitleBar()
.setOAuthToken(authToken)
.setCallback(pickerCallback)
.setOrigin('https://docs.google.com')
.build();
picker.setVisible(true);
} else {
showError('Unable to load the file picker.');
}
}
function pickerCallback(data, ctx) {
var action = data[google.picker.Response.ACTION];
if(action == google.picker.Action.PICKED) {
var doc = data[google.picker.Response.DOCUMENTS][0];
var id = doc[google.picker.Document.ID];
// Option 1 - Advanced Drive Service in apps script
return google.script.run.withSuccessHandler(showMessage).withFailureHandler(showError).getFileWithAdvancedDriveService(id);
// Option 2 - Drive Service in apps script
return google.script.run.withSuccessHandler(showMessage).withFailureHandler(showError).getFileWithDriveService(id);
// Option 3 - Drive Service in client
return gapi.load('client', function () {
gapi.client.load('drive', 'v2', function () {
gapi.client.setToken({ access_token: authToken });
var file = gapi.client.drive.files.get({ 'fileId': id });
file.execute(function (resp) {
showMessage(resp);
});
});
});
} else if(action == google.picker.Action.CANCEL) {
google.script.host.close();
}
}
function showMessage(message) {
document.getElementById('result').innerHTML = '<div>Message:</div> ' + JSON.stringify(message);
}
function showError(message) {
document.getElementById('result').innerHTML = `<div>Error:</div> <div style="color:red;">${message}</div>`;
}
</script>
</head>
<body>
<div>
<p id="result"></p>
</div>
<script type="text/javascript" src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
</body>
</html>
gapi.client.drive.files.get fails with the error:
code: 404, message: "File not found: 1d0cqiT3aipgjMfLPolzgWVrnsl4xPxUJ1_7pH3ONVzU"
I tried the same on the server side (apps script) and got similar error:
DriveApp.getFolderById(fileId)
returns:
You do not have permission to call DriveApp.getFolderById. Required permissions: (https://www.googleapis.com/auth/drive.readonly || https://www.googleapis.com/auth/drive)
Same with advanced drive api:
Drive.Files.get(fileId)
returns:
GoogleJsonResponseException: API call to drive.files.get failed with error: File not found: 1d0cqiT3aipgjMfLPolzgWVrnsl4xPxUJ1_7pH3ONVzU
Do I need drive.readonly scope to read the file opened by the user using Google Picker?
Yes, as the https://www.googleapis.com/auth/drive.file scope only allows you to:
View and manage Google Drive files and folders that you have opened or created with this app
And https://www.googleapis.com/auth/drive.readonly:
See and download all your Google Drive files
You can review the full list of scopes here.
I found the problem. It works if I move this gapi.load code from pickerCallback:
return gapi.load('client', function () {
gapi.client.load('drive', 'v2', function () {
gapi.client.setToken({ access_token: authToken });
var file = gapi.client.drive.files.get({ 'fileId': id });
file.execute(function (resp) {
showMessage(resp);
});
});
});
to javascript onload:
function onGsiLoad()
{
return gapi.load('client', function () {
return gapi.client.load('drive', 'v2', function () {
gsiLoaded = true;
maybeEnablePicker();
});
});
}
And only have gapi.client.drive.files.get in pickerCallback:
var file = gapi.client.drive.files.get({ 'fileId': fileId });
file.execute(function (resp) {
showMessage(resp);
});
Working test case is here: https://docs.google.com/forms/d/1h3FWKVpGbCApg1_2unD3L86QlOmh9CIwK-W1Q97UTYQ/edit?usp=sharing
Buggy one is here:
https://docs.google.com/forms/d/1jpEa-mp8ccCZhEGlgBN52AML2i1oHIShFpY8Oe3GC44/edit?usp=sharing

Drive file picker from the older Google Sign-In platform library to the newer Google Identity Services library for authentication

I have a code where I read the file data as blob. I have implemented using the old gapi, how do I migrate from the older Google Sign-In platform library to the newer Google Identity Services library for authentication.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Google Picker Example</title>
</head>
<body>
<button id="authorize_button" style="display: none;">Authorize</button>
<button id="signout_button" style="display: none;">Sign Out</button>
<div id="result"></div>
<script type="text/javascript" src="script.js"></script>
<script async defer src="https://apis.google.com/js/api.js"
onload="this.onload=function(){};handleClientLoad()"
onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
<script>
const API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const CLIENT_ID = '995979103146-6qdmadbu7ha5ptrthsg8uqonkpplvc8e.apps.googleusercontent.com';
const appId = "995979103146";
const SCOPES = ["https://www.googleapis.com/auth/drive"];
const DISCOVERY_DOCS = [
"https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
];
const authorizeButton = document.getElementById("authorize_button");
const signoutButton = document.getElementById("signout_button");
// Use the Google API Loader script to load the google.picker script.
function handleClientLoad() {
gapi.load("client:auth2:picker", initClient);
}
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES[0]
})
.then(
function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(handleSignIn);
// Handle the initial sign-in state.
handleSignIn(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
},
function (error) {
appendPre(JSON.stringify(error, null, 2));
}
);
}
function handleSignIn(isSignedIn) {
if (isSignedIn) {
authorizeButton.style.display = "none";
signoutButton.style.display = "block";
createPicker();
} else {
authorizeButton.style.display = "block";
signoutButton.style.display = "none";
}
}
function handleAuthClick(event) {
gapi.auth2.getAuthInstance().signIn();
}
function handleSignoutClick(event) {
gapi.auth2.getAuthInstance().signOut();
}
function createPicker() {
const token = gapi.client.getToken().access_token
if (token) {
let view = new google.picker.View(google.picker.ViewId.DOCS);
view.setMimeTypes("image/png,image/jpeg,image/jpg");
let picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.setAppId(appId)
.setOAuthToken(token)
.addView(view)
.addView(new google.picker.DocsUploadView())
.setDeveloperKey(API_KEY)
.setCallback(getFile)
.build();
picker.setVisible(true);
}
}
function getFile(pickerResp) {
console.log(JSON.stringify(pickerResp))
if(pickerResp.action == "picked") {
gapi.client.drive.files
.get({
fileId: pickerResp.docs[0].id,
alt: 'media'
})
.then(resp => {
console.log("fetch response", resp.status)
let binary = resp.body
// EDIT - addition from Gabrielle vvvv
let l = binary.length
let array = new Uint8Array(l);
for (var i = 0; i<l; i++){
array[i] = binary.charCodeAt(i);
}
let blob = new Blob([array], {type: 'application/octet-stream'});
console.log(blob)
// EDIT - addition from Gabrielle ^^^^
});
}
}
</script>
</body>
</html>
It is really easy to implement the new Google Identity Library. As you can compare here, you only have to change a few things.
This example is the easiest approach (implicit flow):
One button for retrieving the token
One button for loading the picker
<html>
<body>
<script
src="https://accounts.google.com/gsi/client"
onload="initClient()"
async
defer
></script>
<script>
var client;
var access_token;
function loadPicker() {
gapi.load('picker', {'callback': ()=>console.log("Picker Loaded")});
}
function initClient() {
client = google.accounts.oauth2.initTokenClient({
client_id: "<CLIENT_ID>",
scope:
"https://www.googleapis.com/auth/drive.file",
callback: (tokenResponse) => {
access_token = tokenResponse.access_token;
},
});
}
function getToken() {
client.requestAccessToken();
}
function revokeToken() {
google.accounts.oauth2.revoke(access_token, () => {console.log('access token revoked')});
}
function onPickerApiLoad() {
var picker = new google.picker.PickerBuilder()
.addView(google.picker.ViewId.DOCS)
.setOAuthToken(access_token)
.build();
picker.setVisible(true);
}
</script>
<!-- The Google API Loader script. -->
<h1>Google Identity Services Authorization Token model</h1>
<button onclick="getToken();">Get access token</button><br /><br />
<button onclick="onPickerApiLoad()">Load picker</button>
<script src="https://apis.google.com/js/api.js?onload=loadPicker"></script>
</body>
</html>
Documentation:
Migrate to Google Identity Services
Google has released a new Code sample I would start there to see the changes for authorizaotn. It should just be a matter of swapping out the authorization methods. The rest of the code is unchanged.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Google Picker Example</title>
<script type="text/javascript">
// The Browser API key obtained from the Google API Console.
// Replace with your own Browser API key, or your own key.
var developerKey = 'xxxxxxxYYYYYYYY-12345678';
// The Client ID obtained from the Google API Console. Replace with your own Client ID.
var clientId = "1234567890-abcdefghijklmnopqrstuvwxyz.apps.googleusercontent.com"
// Replace with your own project number from console.developers.google.com.
// See "Project number" under "IAM & Admin" > "Settings"
var appId = "1234567890";
// Scope to use to access user's Drive items.
var scope = ['https://www.googleapis.com/auth/drive.file'];
var pickerApiLoaded = false;
var oauthToken;
// Use the Google API Loader script to load the google.picker script.
function loadPicker() {
gapi.load('auth', {'callback': onAuthApiLoad});
gapi.load('picker', {'callback': onPickerApiLoad});
}
function onAuthApiLoad() {
window.gapi.auth.authorize(
{
'client_id': clientId,
'scope': scope,
'immediate': false
},
handleAuthResult);
}
function onPickerApiLoad() {
pickerApiLoaded = true;
createPicker();
}
function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
oauthToken = authResult.access_token;
createPicker();
}
}
// Create and render a Picker object for searching images.
function createPicker() {
if (pickerApiLoaded && oauthToken) {
var view = new google.picker.View(google.picker.ViewId.DOCS);
view.setMimeTypes("image/png,image/jpeg,image/jpg");
var picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.setAppId(appId)
.setOAuthToken(oauthToken)
.addView(view)
.addView(new google.picker.DocsUploadView())
.setDeveloperKey(developerKey)
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
}
}
// A simple callback implementation.
function pickerCallback(data) {
if (data.action == google.picker.Action.PICKED) {
var fileId = data.docs[0].id;
alert('The user selected: ' + fileId);
}
}
</script>
</head>
<body>
<div id="result"></div>
<!-- The Google API Loader script. -->
<script type="text/javascript" src="https://apis.google.com/js/api.js?onload=loadPicker"></script>
</body>
</html>

Reusable Google doc Picker in google scripts - Picker Callback

Docu References:
Drive file picker v3
G Suite Dialogs: File Open Dialog
SO References:
Access data in Google App Script from spread sheet modal html form
How do I handle the call back using multiple Google File Picker
What to achieve?
In a Google Sheets script, I would like to define a Files Picker that returns the data of picked up files, provided that thereon, from another part of the scripts, the caller can receive that data.
Problem:
The file picker is launched as an html Modal dialog. After searching for a while, the only solution to get the data from the script that launched the picker is from the html script code:
set the callaback of the picker to a specific function: picker.setCallback(my_callback)
or use google.script.run.my_callback (i.e. from button Done for instance)
... provided that my_callback function defined in your script gets the data.
The problem with the above is that you cannot use the same picker for multiple purposes, because:
my_callback is fixed in the html script
my_callback cannot know for what purpose the picker was initially called (i.e. should it get the content?, should it give the information to some unknown caller?).
Once it gets the data, my_callback does not know what to do with it... unless my_callback is tied to only 1 caller; which does not seem correct, as that would require to have multiple html definitions for the picker, once per each reason you may invoke it, so it can call back to the proper function.
Any ideas?
global variables in scripts get re-initialized and cannot use PropertiesService to store values other than String (so no way to store the final picker_callback through a global var).
google.script.run does not offer calls by giving the name of the server-side function as String (reference) (which discards having a function to generate the picker_dialog.html by changing the callback function).
Sample Code
code.gs
function ui() {
return SpreadsheetApp.getUi();
}
function onOpen() {
ui().createMenu('ecoPortal Tools')
.addItem('Read a file', 'itemReadFile')
.addItem('Edit a file', 'itemEditFile')
.addToUi();
}
function itemReadFile() {
pickFile(readFile)
}
function itemEditFile() {
pickFile(editFile)
}
function readFile(data) {
/* do some stuff */
}
function editFile(data) {
/* do some stuff */
}
picker.gs:
function pickFile(callback) {
var html = HtmlService.createHtmlOutputFromFile('picker_dialog.html')
.setWidth(600)
.setHeight(425)
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
// concept (discarded):
callback_set('picker', callback)
ui().showModalDialog(html, 'Select a file');
}
function getOAuthToken() {
DriveApp.getRootFolder();
return ScriptApp.getOAuthToken();
}
// picker callback hub
function pickerCallback(data) {
var callback = callback_get('picker');
callback_set('picker', null);
if (callback) callback.call(data);
}
picker_dialog.html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<script>
var DEVELOPER_KEY = '___PICKER_API_KEY_____';
var DIALOG_DIMENSIONS = {width: 600, height: 425};
var pickerApiLoaded = false;
// currently selected files data
var files_data = null;
/**
* Loads the Google Picker API.
*/
function onApiLoad() {
gapi.load('picker', {'callback': function() {
pickerApiLoaded = true;
}});
}
function getOAuthToken() {
console.log("going to call get auth token :)");
google.script.run.withSuccessHandler(createPicker)
.withFailureHandler(showError).getOAuthToken();
}
function createPicker(token) {
console.log("pickerApiLoadded", pickerApiLoaded);
console.log("token", token);
if (pickerApiLoaded && token) {
var picker = new google.picker.PickerBuilder()
.addView(google.picker.ViewId.DOCS)
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.hideTitleBar()
.setOAuthToken(token)
.setDeveloperKey(DEVELOPER_KEY)
.setCallback(pickerCallback)
.setOrigin(google.script.host.origin)
.setSize(DIALOG_DIMENSIONS.width - 2,
DIALOG_DIMENSIONS.height - 2)
.build();
picker.setVisible(true);
} else {
showError('Unable to load the file picker.');
}
}
function pickerCallback(data) {
var action = data[google.picker.Response.ACTION];
if (action == google.picker.Action.PICKED) {
files_data = data;
var doc = data[google.picker.Response.DOCUMENTS][0];
var id = doc[google.picker.Document.ID];
var url = doc[google.picker.Document.URL];
var title = doc[google.picker.Document.NAME];
document.getElementById('result').innerHTML =
'<b>You chose:</b><br>Name: <a href="' + url + '">' + title +
'</a><br>ID: ' + id;
} else if (action == google.picker.Action.CANCEL) {
document.getElementById('result').innerHTML = 'Picker canceled.';
}
}
function showError(message) {
document.getElementById('result').innerHTML = 'Error: ' + message;
}
function closeIt() {
google.script.host.close();
}
function returnSelectedFilesData() {
google.script.run.withSuccessHandler(closeIt).pickerCallback(files_data);
}
</script>
</head>
<body>
<div>
<button onclick='getOAuthToken()'>Select a file</button>
<p id='result'></p>
<button onclick='returnSelectedFilesData()'>Done</button>
</div>
<script src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
</body>
</html>
picker.setCallback(my_callback)
Picker callback is different from:
or use google.script.run.my_callback
The former calls a function on the frontend html while the latter calls a function in the server.
my_callback cannot know for what purpose the picker was initially called
You can send a argument to the server:
google.script.run.my_callback("readFile");
On the server side(code.gs),
fuction my_callback(command){
if(command === "readFile") Logger.log("Picker called me to readFile");
}
google.script.run does not offer calls by giving the name of the server-side function as String
Not true. Dot is used to access members of a object. You can use bracket notation to access a member as a string:
google.script.run["my_callback"]();
EDITED BY Q.ASKER:
In your case, to pass the files_data to the server side:
google.script.run.withSuccessHandler(closeIt)[my_callback](files_data);
Now, for my_callback (String variable) to be set from server side, you need to push it using templates:
function pickFile(str_callback) {
var htmlTpl = HtmlService.createTemplateFromFile('picker_dialog.html');
// push variables
htmlTpl.str_callback = str_callback;
var htmlOut = htmlTpl.evaluate()
.setWidth(600)
.setHeight(425)
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
ui().showModalDialog(htmlOut, 'Select a file');
}
The two unique changes that you need to make to your picker_dialog.html:
add printing scriptlet to set my_callback (<?= ... ?>)
use the google.script.run as mentioned
var my_callback = <?= str_callback? str_callback : 'defaultPickerCallbackToServer' ?>;
/* ... omitted code ... */
function returnSelectedFilesData() {
google.script.run.withSuccessHandler(closeDialog)[my_callback](files_data);
}
Now, when you call pickFile to open the frontend picker, you are able to set a different server callback that will receive the data with the file(s) chosen by the user.

Send drive folder list to listbox HTML

I have a script that is pulling all the folders in my drive. I want to push this data into a listbox that has been created in HTML (UiApp is deprecated)
The code I have is (note that the sheet.appendRow line doesn't work and if you know why and feel like explaining that would be awesome!!)
function updateFoldersListbox(FoldersListBox) {
var rootFolder = DriveApp.getRootFolder();
var driveFolders = rootFolder.getFolders();
while (driveFolders.hasNext()) {
var addMonitoredFolder = driveFolders.next();
Logger.log(addMonitoredFolder);
//sheet.appendRow(addMonitoredFolder);
}
The output of the logs is:
[15-09-26 01:38:46:603 BST] Test Folder
[15-09-26 01:38:46:605 BST] Untitled folder
[15-09-26 01:38:46:606 BST] Notes
Now to push that to a listbox and add test folder, untitled folder and notes as an option:
<select name="sometext" size="5">
<option>option1</option>
</select>
How is that done?
Put the following code in a script file:
function onOpen() {
SpreadsheetApp.getUi().createMenu('Dialog').addItem('Open', 'openDialog').addToUi();
};
function openDialog() {
var html = HtmlService.createHtmlOutputFromFile('Index')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
SpreadsheetApp.getUi().showModalDialog(html, 'Select a Google Drive Folder:');
};
function updateFoldersListbox() {
var rootFolder = DriveApp.getRootFolder();
var driveFolders = rootFolder.getFolders();
var listnames = new Array();
while (driveFolders.hasNext()) {
var addMonitoredFolder = driveFolders.next().getName();
listnames.push([addMonitoredFolder]);
//Logger.log(addMonitoredFolder);
//sheet.appendRow(addMonitoredFolder);
}
return listnames;
};
And put the following code in a HTML file 'Index.html':
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<select id="menu"> </select>
<script
src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
</script>
<script>
// The code in this function runs when the page is loaded.
$(function() {
google.script.run.withSuccessHandler(showThings)
.updateFoldersListbox();
google.script.run.withSuccessHandler(showMenu)
.updateFoldersListbox();
});
function showThings(things) {
var list = $('#things');
list.empty();
for (var i = 0; i < things.length; i++) {
list.append('<li>' + things[i] + '</li>');
}
}
function showMenu(menuItems) {
var list = $('#menu');
list.find('option').remove(); // remove existing contents
for (var i = 0; i < menuItems.length; i++) {
list.append('<option>' + menuItems[i] + '</option>');
}
}
</script>
</body>
</html>
Now, run the function 'openDialog' from the script editor.
Or refresh your spreadsheet and run the function from the custom menu 'Dialog' > 'Open'.

How to pass a parameter to html?

I have a script that uses the file picker but I need to pass a specific parameter which is called userId and is kept as a global variable in the calling script. As the calls are asynchronous it seems I cannot access this parameter. Is there a way to access the parameter from the html file or pass this parameter to the html?
I might be mixing templated html and non templated.
Here is the calling code (initiated through a menu item in a spreadsheet):
function syncStudentsFile(userId, ss) {
scriptUser_(userId); // save userId
Logger.log('SRSConnect : syncStudentsFile : userId:'+userId); // userId is correct here
var ss = SpreadsheetApp.getActiveSpreadsheet();
var html = HtmlService.createHtmlOutputFromFile('PickerSync.html')
.setWidth(600).setHeight(425);
SpreadsheetApp.getUi().showModalDialog(html, 'Select a file');
}
function scriptUser_(userId) {
if (userId !== undefined)
sUserId = userId; // Global variable
try { return sUserId; } catch (e) { return undefined; }
}
function getOAuthToken() { // used by Picker
DriveApp.getRootFolder();
return ScriptApp.getOAuthToken();
}
Here is the html picker file:
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<script type="text/javascript">
var DEVELOPER_KEY = '..............';
var DIALOG_DIMENSIONS = {width: 600, height: 425};
var pickerApiLoaded = false;
/**
* Loads the Google Picker API.
*/
gapi.load('picker', {'callback': function() {
pickerApiLoaded = true;
}});
/**
* Gets the user's access token from the server-side script so that
* it can be passed to Picker. This technique keeps Picker from needing to
* show its own authorization dialog, but is only possible if the OAuth scope
* that Picker needs is available in Apps Script. Otherwise, your Picker code
* will need to declare its own OAuth scopes.
*/
function getOAuthToken() {
google.script.run.withSuccessHandler(createPicker)
.withFailureHandler(showError).getOAuthToken();
}
/**
* Creates a Picker that can access the user's spreadsheets. This function
* uses advanced options to hide the Picker's left navigation panel and
* default title bar.
*
* #param {string} token An OAuth 2.0 access token that lets Picker access the
* file type specified in the addView call.
*/
function createPicker(token) {
if (pickerApiLoaded && token) {
var uploadView = new google.picker.DocsUploadView();
var picker = new google.picker.PickerBuilder()
// Instruct Picker to display only spreadsheets in Drive. For other
// views, see https://developers.google.com/picker/docs/#otherviews
.addView(google.picker.ViewId.DOCS)
.addView(google.picker.ViewId.RECENTLY_PICKED)
.addView(uploadView)
.hideTitleBar()
.setOAuthToken(token)
.setDeveloperKey(DEVELOPER_KEY)
.setCallback(pickerCallback)
// Instruct Picker to fill the dialog, minus 2 pixels for the border.
.setSize(DIALOG_DIMENSIONS.width - 2,
DIALOG_DIMENSIONS.height - 2)
.build();
picker.setVisible(true);
} else {
showError('Unable to load the file picker.');
}
}
/**
* A callback function that extracts the chosen document's metadata from the
* response object. For details on the response object, see
* https://developers.google.com/picker/docs/result
*
* #param {object} data The response object.
*/
function pickerCallback(data) {
var action = data[google.picker.Response.ACTION];
if (action == google.picker.Action.PICKED) {
var doc = data[google.picker.Response.DOCUMENTS][0];
var id = doc[google.picker.Document.ID];
google.script.host.close();
// --------------> user global parameter sUserId set earlier
google.script.run.PickerSyncFile(sUserId, id);
} else if (action == google.picker.Action.CANCEL) {
google.script.host.close();
}
}
/**
* Displays an error message within the #result element.
*
* #param {string} message The error message to display.
*/
function showError(message) {
document.getElementById('result').innerHTML = 'Error: ' + message;
}
</script>
<div>
<script>getOAuthToken()</script>
<p id='result'></p>
<input type="button" value="Close" onclick="google.script.host.close()" />
</div>
Here is the picker code:
function pickerSyncFile(userId, id) {
Logger.log('userId:'+userId); // BUG: it is null
Logger.log('id:'+id); // id returned well from picker
// rest of code here but userId was is incorrect
}
The safest way is to pass the needed data to the HTML directly. If you use properties or cache service it can get complex or fail under multiple simultaneous users.
There are many techniques to pass an initial object from the server (.gs) to the client (.html).
Using HtmlTemplate, you may do:
//.gs file
function doGet() {
var htmlTemplate = HtmlService.createTemplateFromFile('template-client');
htmlTemplate.dataFromServerTemplate = { first: "hello", last: "world" };
var htmlOutput = htmlTemplate.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('sample');
return htmlOutput;
}
and in your template-client.html file:
<!DOCTYPE html>
<script>
var data = <?!= JSON.stringify(dataFromServerTemplate) ?>; //Stores the data directly in the javascript code
// sample usage
function initialize() {
document.getElementById("myTitle").innerText = data.first + " - " + data.last;
//or use jquery: $("#myTitle").text(data.first + " - " + data.last);
}
// use onload or use jquery to call your initialization after the document loads
window.onload = initialize;
</script>
<html>
<body>
<H2 id="myTitle"></H2>
</body>
</html>
It is also possible to do it without using templating, by appending a hidden div to an HtmlOutput:
//.gs file:
function appendDataToHtmlOutput(data, htmlOutput, idData) {
if (!idData)
idData = "mydata_htmlservice";
// data is encoded after stringifying to guarantee a safe string that will never conflict with the html.
// downside: increases the storage size by about 30%. If that is a concern (when passing huge objects) you may use base94
// or even base128 encoding but that requires more code and can have issues, see http://stackoverflow.com/questions/6008047/why-dont-people-use-base128
var strAppend = "<div id='" + idData + "' style='display:none;'>" + Utilities.base64Encode(JSON.stringify(data)) + "</div>";
return htmlOutput.append(strAppend);
}
// sample usage:
function doGet() {
var htmlOutput = HtmlService.createHtmlOutputFromFile('html-sample')
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('sample');
// data can be any (serializable) javascript object.
// if your data is a native value (like a single number) pass an object like {num:myNumber}
var data = { first: "hello", last: "world" };
// appendDataToHtmlOutput modifies the html and returns the same htmlOutput object
return appendDataToHtmlOutput(data, htmlOutput);
}
and in your output-client.html:
<!DOCTYPE html>
<script>
/**
* getDataFromHtml
*
* Inputs
* idData: optional. id for the data element. defaults to "mydata_htmlservice"
*
* Returns
* The stored data object
*/
function getDataFromHtml(idData) {
if (!idData)
idData = "mydata_htmlservice";
var dataEncoded = document.getElementById(idData).innerHTML;
var data = JSON.parse(atob(dataEncoded));
return data;
}
// sample usage of getDataFromHtml
function initialize() {
var data = getDataFromHtml();
document.getElementById("myTitle").innerText = data.first + " - " + data.last;
//or use jquery: $("#myTitle").text(data.first + " - " + data.last);
}
// use onload or use jquery to call your initialization after the document loads
window.onload = initialize;
</script>
<html>
<body>
<H2 id="myTitle"></H2>
</body>
</html>
Both methods are compared and better explained in this little github I made:
https://github.com/zmandel/htmlService-get-set-data
I often use HtmlService templates to push static values to the client.
index.html
<script>
var domain = "<?=domain?>";
</script>
code.gs
var ui = HtmlService.createTemplateFromFile('Sidebar');
ui.domain = domain;
return ui.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME).setTitle(strings[lang][domain]);
In your code:
function scriptUser_(userId) {
if (userId !== undefined)
sUserId = userId; // Global variable
try { return sUserId; } catch (e) { return undefined; }
}
You are assigning a value to the global variable named sUserId. But, then when you try to retrieve it, nothing is there. Global variables loose their value as soon as the current instance of the code bring run is completed. Global variable don't persist their values.
You'll need to use the Properties Service to store the value. Or you could use the Cache service. If you want the value of the user id to expire after some time, use cache service.
By Appending to the HTML File, as shown below.
within Code.gs
function showDialog() {
var html = HtmlService.createHtmlOutputFromFile('html-sample')
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setWidth(600)
.setHeight(425);
var data = "Hello World!";
var strAppend = "<div id='id_for_div' style='display:none;'>" + data + "</div>";
html.append(strAppend);
var title = "Demo";
SpreadsheetApp.getUi().showModalDialog(html, title); // or DocumentApp or SlidesApp or FormApp.
}
html-sample.html
<!DOCTYPE html>
<html>
<head>
<script>
function setText(text) {
var element = document.getElementById("myid");
element.innerHTML = text;
}
function getDataFromHtml() {
var id = "id_for_div";
var dataEncoded = document.getElementById(id).innerHTML;
setText(dataEncoded);
}
</script>
</head>
<body>
<h1 id="myid">Sample Text</h1>
<button onclick="getDataFromHtml()">Try Now!</button>
</body>
</html>
Click the 'Try Now!' Button and See the Magic!
This post provides the solution how to pass parameter to a templated html:
html = HtmlService.createTemplateFromFile('page2');
html.id = s1;
Then in page 2 use the the tag to print out the id value;
<div class="info" >
<span id="ID" name="ID"><?=id?></span>
</div>