Unpicking the Google Picker


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

  };




Why not join our community , follow the blogtwitterG+  and let me know.

Comments