When you first use the Google Picker with Apps Script it can be quite daunting to figure out how to do what, and in what order, especially as there a few error reported in the developers console that look serious, but can be ignored. As far as I can tell from reading various posts and forums they seem to be the result of a difference of opinion between various teams – but who knows. In any case these are the errors you can ignore. If you get your coding right and you got the picker working without checking the console, then you may not have noticed this, but getting it right first time with the Picker can be tough – so like many, I wasted many hours thinking the problem with my code was related to this message, rather than the fairly obvious unrelated bug in my code. The problem with the Picker, is that you often find yourself building the logic of your App into the Picker process, and the logic of the Picker into your App. In an effort to standardize my Picker implementations and ‘unpick the picker’ (and so that I never have to look at it again), I’ve converted the whole messy business to use Promises with a single interface point for each kind of dialog, so this post will cover that pattern.
Getting started
The picker is a separate API to Drive, so needs to be enabled in the cloud console for your project. Although not mandatory, you should also create a developer api key with google.com restrictions to use with the picker For the purposes of my SlidesMerge add-on I’m keeping this in my property store The Picker API needs Drive scope, so the simplest way to do that is to invoke a Drive operation back in your script which will authorize your script for that scope – and you can just borrow its token over on the client side to use with the Picker. My project uses Drive plus a few more scopes, so its access token will do just fine. Once you’ve done all that, you’re ready to code. The full code referred to here is available on github and I use many of my own utility functions which I won’t bother going into here, but you are welcome to reuse.
Invoking the picker
This normally happens as a click event on a button. Note that UsePicker.presentationDialog() returns a Promise, so there’s no need to pollute the Picker management code with all kinds of global variables or callbacks, and for all my future projects this is all I’ll have to do to integrate the Picker into them. It returns a packet with information about the file, or not if the operation was cancelled. // select a template as input
DomUtils.elem("presentation-button")
.addEventListener ('click', function (e) {
UsePicker.presentationDialog()
.then (function (packet) {
if (packet.id) {
// a file was picked .. do something
Client.settings.deck.templatePacket = packet;
}
///.... etc
});
});
What I like about this approach is that it’s ‘code an forget’. In the future using the Picker will be as simple as this UsePicker.folderDialog()
.then (function (pickerFile) {
// do something with the picker file
});
Initialization
Somewhere in the your main function, you need to initialize the Picker like this before you can use it. // we'll need the picker later
UsePicker.init();
These things happen asynchronously so there’s no need to bother about them for now – the settings property of the namespace will eventually contain a resolved Promise for each of loading the gapi Picker library and getting the API key from the Server Property store. /**
* all about using the Google Picker
* the API I dislike above all others
*/
var UsePicker = (function (ns) {
/**
* init the picker dialog
*/
ns.settings = {
dialog: {
width:600,
height:400
},
promises: {
}
};
// Initialize the picker
// get a token & make sure gapi gets loaded for later
ns.init = function () {
var sp = ns.settings.promises;
// and load the picker
sp.gapi = new Promise (function (resolve, reject) {
gapi.load('picker', {callback:resolve});
});
// and the developer key
sp.devKey = Provoke.run ('PropertyStores', 'get' , 'script' , 'pickerDeveloperKey')
.then (function (result) {
// nothing to do for now
return result;
})
['catch'](function (err) {
App.showNotification ("getting developerKey" , err);
});
};
The Picker dialog
My app needs 2 types of dialog – a folder dialog and a file dialog. It turns out they are very similar My folder Dialog only looks at folder I own. ns.folderDialog = function () {
var docsView = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
.setSelectFolderEnabled(true)
.setIncludeFolders(true)
.setOwnedByMe(true);
return ns.dialog (docsView);
};
My file dialog looks at files I own or that have been shared with me ns.presentationDialog = function () {
var docsView = new google.picker.DocsView(google.picker.ViewId.PRESENTATIONS)
.setIncludeFolders(true)
return ns.dialog (docsView);
};
And they share a common Picker dialog, the main annotated points of which are
- A – If nothing has happened for a while, the access token we got last time may have expired, so get another from the server
- B – Since we going to return a promise, we can construct a new Promise and remember its resolve and reject callbacks to use later
- C – Before we can use the Picker we need the Picker library loaded, an access token, and the dev key from the Server Properties store. These were all initiated earlier so Promise.all will execute when they are all complete.
- D – the Picker callback is converted into a promise by invoking the resolve callback we reserved earlier. Note that the callback is invoked not only when the file is picked or the picker is cancelled, but also when it is loaded – so we only want to resolve the callback on cancelled or picked.
- E – For my Apps, I generally want to get a few more attributes on the file than is returned by the picker, so I call the Drive API to get them, using the same access token I’m using for the Picker. I’m using axios since I use it elsewhere in the app anyway, but you could also use gapi if you need to do this. This is also an asynchronous activity so the resolution of that is required before the picker is finally marker as resolved.
- F – this is required to allow the Picker to allow origin from an alternate domain (yet the error in the console implies it didn’t work 100%). The google.script.host.origin actually resolves to docs.google.com even though your client app will be something like xxx.googleusercontent.com
ns.dialog = function (view) {
var sp = ns.settings.promises, resolve, reject;
// A..........
// get a new token in case the old one expired
sp.token = Provoke.run ('Server', 'getOAuthToken')
.then (function (result) {
// nothing to do for now
return result;
})
['catch'](function (err) {
App.showNotification ("getting access token" , err);
});
// B........
// sort out the picker to return promises
var pr = new Promise (function (res, rej) {
resolve = res;
reject = rej;
});
// C...
// when all is is ready, then bringup the picker
return Promise.all ([sp.gapi, sp.token , sp.devKey])
.then (function (r) {
var token = r[1];
var devKey = r[2];
if (!token || !devKey) throw 'couldnt get credentials';
// set up the picker
var picker = new google.picker.PickerBuilder()
.addView(view)
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.hideTitleBar()
.setOAuthToken(token)
.setDeveloperKey(devKey)
.setSize(ns.settings.dialog.width,ns.settings.dialog.height)
// D........
// the callback transformed into a promise
.setCallback(function (data) {
// called when something is picked
// note that you get called on loaded to, so just resolve
// when we have a picked or cancelled
var action = data[google.picker.Response.ACTION];
var picked = action == google.picker.Action.PICKED;
var cancelled = action == google.picker.Action.CANCEL;
var doc = picked && data[google.picker.Response.DOCUMENTS][0];
var id = doc && doc[google.picker.Document.ID];
var package = {
action:action,
picked:picked,
doc:doc,
id:id
};
// get a thumbnail and parents using drive API
if (id) {
/// E.............
var url = "https://www.googleapis.com/drive/v2/files/" + id;
axios.get(url, {
headers: {
authorization: "Bearer " + token
}
})
.then(function (response) {
package.thumbnail = response.data.thumbnailLink;
package.title = response.data.title;
package.iconLink = response.data.iconLink;
package.parents = response.data.parents;
resolve (package);
})
['catch'](function (error) {
reject(error);
});
}
else if (cancelled) {
resolve (package);
}
})
/// F..........
.setOrigin(google.script.host.origin)
.build();
picker.setVisible(true);
return pr;
})
['catch'](function (err) {
reject (err);
});
};
Subpages
- Chord Snip
- Color Arranger
- Debugging Office JavaScript API add-ins
- Dicers
- Dicers Pro and advanced features
- Measure round trip and execution time from add-ons
- Merging slide templates with tabular data
- Office Add-ins – first attempt
- Orchestrating competing google and Office framework loads
- Plotting maps with overlays Sheets add-on starter
- Promise implementation for Apps Script Stripe payments
- Repeatable add-on settings layouts and style
- Sheets API – Developer Metadata
- SlidesMerge add-on
- Watching for changes in an Office add-in
- When test add-ons doesn’t work
- Polyfill for Apps Script properties service for the Office JavaScript API
- Sankey Snip