Search code examples
google-apps-scriptgoogle-picker

Reusable Google doc Picker in google scripts - Picker Callback


Docu References:

  1. Drive file picker v3
  2. G Suite Dialogs: File Open Dialog

SO References:

  1. Access data in Google App Script from spread sheet modal html form
  2. 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:

  1. set the callaback of the picker to a specific function: picker.setCallback(my_callback)
  2. 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:

  1. my_callback is fixed in the html script
  2. 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>


Solution

  • 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:

    1. add printing scriptlet to set my_callback (<?= ... ?>)
    2. 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.