I am supporting CandidateX

CandidateX is a startup that focuses on creating inclusion-focused hiring solutions, designed to increase access to job opportunities for underestimated talent. Check them out if you have a few minutes to spare. They need visibility!

 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  }); 


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);    });  };