Managing Google Drive with a Google Apps Script and Add-Ons

Written by kcl | Published 2022/08/23
Tech Story Tags: google-apps-script | gsuite | google-drive | javascript | oauth | oauth2 | contextual-card | gcp | web-monetization

TLDRLet's extend Google Drive with Apps Script to create a simple add-on. We'll use CardService for the UI, where we'll select a few spreadsheets and pass them to the next card with navigation. You'll need knowledge of JavaScript and Access to google drive. I am using apps script ide but if you want to develop in the local environment you'll find this [set-up](https://kcl.hashnode.dev/how-to-write-google-apps-script-code-locally) guide helpful.via the TL;DR App

Let's extend Google Drive with Apps Script to create a simple add-on, and use CardService for the UI, where we'll select a few spreadsheets and pass them to the next card with navigation. You can find my other blogs on Google Apps Scripts right here.

Intro

Hi, This is Nibes Khadka, from Khadka's Coding Lounge. I wrote this blog because I find the documentation overwhelming for beginners. It is also very hard to find blogs on google apps scripts. So, hence this beginner-level blog was created to get you started. I believe this blog will give you almost 20% you'll need to finish almost 80% of your projects.

Pre-Requisite

You'll need knowledge of JavaScript and Access to google drive. I am using apps script ide but if you want to develop in the local environment you'll find this set-up guide helpful.

Settings

Head over to the dashboard, and create a new script file for the project. After that, we'll need to prep our projects as instructed below.

HomePages

According to the documentation, there're two types of homepages when you're developing add-ons for drive: Contextual and Non-Contextual.

Non-Contextual is an initial display when nothing is happening like the first screen to show after the add-on is clicked. Contextual is the home page/display that shows up once we perform a specific action like selecting files in the drive.

For the apps scripts functions to get called in the drive, we'll need to assign those functions to the appropriate triggers for the drive add-on in the manifest(appsscript.json) file.

Homepage Triggers

When a user clicks on the add-on icon, drive.homepage triggers method is called upon. This method then looks for a function then calls the specified function in the manifest(appsscript.json) for further operations.

Item Selected Trigger

For contextual triggers, we'll assign the function in our apps script to drive.onItemSelectedTrigger in the manifest file.

OAuth Scopes

For the drive add-on to work the user must grant access permission. The list of permissions is known as Scopes. Details on drive-specific scopes can be found here. We'll need to provide the scopes in the appsscript.json file again as a list with "OAuth scopes".

Note: If your appsscript.json file is hidden then go to settings, then check the Show "appsscript.json" manifest file in the editor checkbox.

Check out the manifest file for this project below.

{
 "timeZone": "Asia/Kathmandu",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.storage",
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/drive.file",
    "https://www.googleapis.com/auth/drive.addons.metadata.readonly"
  ],
  "addOns": {
    "common": {
      "name": "Drive Extension with Apps Script",
      "logoUrl": "provide image URL to be used as logo",
      "layoutProperties": {
        "primaryColor": "#41f470",
        "secondaryColor": "#ab2699"
      }
    },
    "drive": {
      "homepageTrigger": {
        "runFunction": "onDriveHomePageOpen",
        "enabled": true
      },
      "onItemsSelectedTrigger": {
        "runFunction": "onDriveItemsSelected"
      }
    }
  }
}

Using Apps Script to Access Google Drive

Now, on the root project folder create two files, cards and main.

Assigning Apps Scripts Functions to the Trigger’s

main

// On homepage trigger function
let onDriveHomePageOpen = () => homepageCard();

// On Item selected Trigger function
let onDriveItemsSelected = (e) => itemSelectedCard(e);

The onDriveHomePageOpen and onDriveItemsSelected are two functions we assigned in the manifest file earlier on. These functions are in turn calling other functions which we'll create in a while. If you get an error pop-up on saving the file, dismiss it for now.

Designing Behaviour of Cards

Let's create a simple homepage card to be assigned to our non-contextual trigger on the cards file.

Create Homepage Card

let homepageCard = () => {
// Create a card with a header section
  let card = CardService.newCardBuilder().setHeader(CardService.newCardHeader());
// create card section 
  let section = CardService.newCardSection();

// add heading 
  let decoratedText = CardService.newDecoratedText()
    .setText("Select Files To Update");

// add text as a widget to the card section
  section.addWidget(decoratedText);

// add the section to the card 
  card.addSection(section);

// return card as build
  return card.build();

}

Cards can be used to create UI for the add-ons for google drive.

This is a beginners blog so I am not focused on styling.

Create Non-Contextual Card

Now, let's have another card that we'll be responsible for the contextual trigger on the same file. But let's divide this code into sections to understand it clearly.

1. Create a simple card UI.
let itemSelectedCard = (e) => {

  // Initial UI
  let card = CardService.newCardBuilder().setHeader(CardService.newCardHeader().setTitle("Select Sheets Update Master Sheet"));
  let filesSection = CardService.newCardSection()
  filesSection.setHeader("Selected Files");
  return card.build();
}

2. Display Selected files in UI.
let itemSelectedCard = (e) => {

  // Initial UI
  let card = CardService.newCardBuilder().setHeader(CardService.newCardHeader().setTitle("Select Sheets Update Master Sheet"));
  let filesSection = CardService.newCardSection()
  filesSection.setHeader("Selected Files");

 // New Code starts here 

// # 1
// Create new array to hold selected files data
  let selectedSheets = [];

// #2
// Fetch selected files data from drive through event objects
  if (e.drive.selectedItems.length > 0) {

    // Selected spreadsheets
// #3
// Among the selected items we'll be selecting only spreadsheets and push them to selected sheets
    e.drive.selectedItems.forEach(item => {
      if (item.mimeType === "application/vnd.google-apps.spreadsheet")
        selectedSheets.push(item)
    }
    );
  }

  // Create a counter to count the number of widgets added
// #4
// COunter is required to prevent error when pushing the file names into UI incase array is empty
  let widgetCounter = 0;

  for (let i = 0; i < selectedSheets.length; i++) {
    // #5
    // Create decorated text with selected files and 
    // add the decorated text to the card section
    filesSection.addWidget(CardService.newDecoratedText()
      //.setText(selectedSheets[i].title)
      .setText(e.drive.selectedItems[0].title)

    );

 // Increase widget counter per loop
 // #4
    widgetCounter += 1;
  }

  // #6
  // Add files as widgets only if widgetCounter is 1+
  //  It prevent error in case only non-spreadsheet files are selected 
  if (widgetCounter >= 1) {
    card.addSection(filesSection)
 }

  // Create Another card that has files list 
  return card.build();
}

Here(see the code for numbering like #1),

  1. Created an array to hold data on selected items.
  2. Used drive event object to fetch data on selected files.
  3. Among the selected items we filtered only spreadsheets using mimeType.
  4. We created a counter to use as a condition while adding the files as widgets in the card to prevent errors.
  5. Created a decorated text, a widget, which will hold the file names of each file.
  6. Now finally added the whole files section to the card builder.

Generate Actions With Button

In Card, interactivity is possible with actions. Also, check out this sample code. Don't forget to add the scope given there, to drive the scope in your manifest file.

Let's add buttons below the files section. This button will collect selected files and pass them to another card which we'll build later on. To less complicate things, I'll break down code into smaller sections.

1. Create Button Ui with Action
  let nxtButtonSection = CardService.newCardSection();
  let nxtButtonAction = CardService.newAction()
    .setFunctionName("handleNextButtonClick");

You've noticed that handleNextButtonClick has been assigned as the function to be triggered on button click. It will handle the navigation, and points toward the next card. We'll create this function later on.

2. Assign Parameters to be passed.
 // We'll pass only pass ids of files to the next card so that we can fetch them there with id
// #1
  let selectedSheetsIDAsStr = selectedSheets.map(item => item.id).join();

// pass the values as params
// #2
  nxtButtonAction.setParameters({
    "nextCard": "nextCard",
    "selectedSheetsIDAsStr": selectedSheetsIDAsStr,
  });

// add button to the button set 
// #3
  let nxtButton = CardService.newTextButton().setText("Next").setOnClickAction(nxtButtonAction);
  let nxtButtonSet = CardService.newButtonSet().addButton(nxtButton);

In the card, parameters need to be passed via action with set parameters method as objects(#2). It's important to remember that both keys and values should be a string(hence #1). Buttons can be added as a button set in the card(#3).

You've noticed that nextCard has been assigned as a parameter. That's because the function handleNextButtonClick is a general function that takes the name of the card as a parameter instead of hardcoding. This way it will be more efficient in the long run.

Add Button To Card
//  It prevent error in case only non-spreadsheet files are selected 
  if (widgetCounter >= 1) {
    card.addSection(filesSection)

    // new line
    nxtButtonSection.addWidget(nxtButtonSet);
    card.addSection(nxtButtonSection);
  }

Card Navigation

From what I understood card navigation, in short, takes a list of cards as a stack. A new card to navigate to is added to the top of the stack, which popped from the stack to return to the previous one.

Let's create a new file, I'll name it helpers, and add the following instructions.

helpers

/* This is a greneral nav function
You use it with card action and as a response, it will supply card functions from cardsInventory */
let handleNextButtonClick = (e) => {

// #1
// Extract string nextCard to pass it as key in cards inventory obj
  let nxtCard = cardsInventory[e.commonEventObject.parameters.nextCard];

  // #2
  // Convert String into List of files selected by the user
  let selectFilesIdList = e.commonEventObject.parameters['selectedSheetsIDAsStr'].split(",");

// #3
// use actionResponse to create a navigation route to the next card
  let nxtActionResponse = CardService.newActionResponseBuilder()
    .setNavigation(CardService.newNavigation().pushCard(nxtCard(selectFilesIdList))) // #4, Passing the mastersheet with params
    .setStateChanged(true)
    .build();

  return nxtActionResponse;
}


/**
 *  Create a dictionary that
 is consist of cards for navigation with appropriate keys  
 */

var cardsInventory = {
  'nextCard': nextCard
}

Let's first talk about the card inventory object. If you remember we passed the parameter nextCard previously as a string in the itemSelectedCard function. This nextCard is the function we'll create next. But the thing is you can't pass a string and use it to reference a variable(check #1 in the code). So, we're creating a dictionary that'll match appropriate keys with functions for navigation.

Inside handleNextButtonClick function:

  1. Extract the string which is key to the cardInventory object to fetch the correct card to call. We're using Events Comment Object to extract parameters passed on earlier.
  2. The string that was passed as selected files id's, we're again converting to the array.
  3. NewActionResponseBuilder, SetNavigation, NewNavigation, and PushCard combined are used to set a new path to the card of our choosing.
  4. Here, we're passing a list of ids as params.

Next Card To Navigate

We'll create a very simple card just enough to display the list of IDs, to let us know our code is working.

First, let's create a new file next_card.

var nextCard = function (lst) {

  let cardService = CardService.newCardBuilder().setHeader(CardService.newCardHeader().setTitle("Select Master Sheet To Update"));

  let filesSection = CardService.newCardSection();

  filesSection.setHeader("Selected Files");

  let widgetCounter = 0;

  let selectedFilesList = [...lst];

  selectedFilesList.forEach(id => {
    filesSection.addWidget(CardService.newDecoratedText()
      .setText(id));
    widgetCounter += 1;
  });

  if (widgetCounter >= 1) {
    cardService.addSection(filesSection);
  }


  return cardService.build();
}

The only thing new to notice here is that I am not using es6 syntax to declare a function. That's because using it caused a scoping issue and the error, the function is not defined. Hence, I went to the old school with var.

Publish Add-On in GCP for Testing

To publish an add-on to the GCP follow these two instructions here.

  1. Create a standard GCP project.
  2. Integrate a project with an apps script project.

Final Code

cards

let itemSelectedCard = (e) => {

  // Initial UI
  let card = CardService.newCardBuilder().setHeader(CardService.newCardHeader().setTitle("Select Sheets Update Master Sheet"));
  let filesSection = CardService.newCardSection()
  filesSection.setHeader("Selected Files");

  let nxtButtonSection = CardService.newCardSection();
  let nxtButtonAction = CardService.newAction()
    .setFunctionName("handleNextButtonClick");
  let selectedSheets = [];

  if (e.drive.selectedItems.length > 0) {
    // hostApp,clientPlatform,drive,commonEventObject
    // Selected spreadsheets
    e.drive.selectedItems.forEach(item => {
      if (item.mimeType === "application/vnd.google-apps.spreadsheet")
        selectedSheets.push(item)
    }
    );
  }

  // Create a counter to count number of widgets added
  let widgetCounter = 0;

  for (let i = 0; i < selectedSheets.length; i++) {
    // Create decorated text with selected files and 
    // add the decorated text to card section
    filesSection.addWidget(CardService.newDecoratedText()
      //.setText(selectedSheets[i].title)
      .setText(e.drive.selectedItems[0].title)

    );
    widgetCounter += 1;
  }


  // Change list of  selected sheet's id  as string to pass to next card 
  let selectedSheetsIDAsStr = selectedSheets.map(item => item.id).join();

  nxtButtonAction.setParameters({
    "nextCard": "nextCard",
    "selectedSheetsIDAsStr": selectedSheetsIDAsStr,
  });

  let nxtButton = CardService.newTextButton().setText("Next").setOnClickAction(nxtButtonAction);
  let nxtButtonSet = CardService.newButtonSet().addButton(nxtButton);


  // Add files and button section only if the widgets are present
  //  It prevent error in case only non-spreadsheet files are selected 
  if (widgetCounter >= 1) {
    card.addSection(filesSection)

    nxtButtonSection.addWidget(nxtButtonSet);
    card.addSection(nxtButtonSection);
  }

  // Create Another card that has files list 
  return card.build();
}

helpers

/* THis is a greneral nav function
You use it with card action and as reponse it will supply card functions from cardsInventory */
let handleNextButtonClick = (e) => {

  let nextCard = cardsInventory[e.commonEventObject.parameters.nextCard];
  console.log(nextCard)

  // Convert String into List
  let selectFilesIdList = e.commonEventObject.parameters['selectedSheetsIDAsStr'].split(",");

  let nxtActionResponse = CardService.newActionResponseBuilder()
    .setNavigation(CardService.newNavigation().pushCard(nextCard(selectFilesIdList)))
    .setStateChanged(true)
    .build();

  return nxtActionResponse;
}


/**
 *  Create a dictionary that
 is consist of cards for navigation with appropriate keys  
 */

var cardsInventory = {
  'nextCard': nextCard
}

Summary

Alright, let's recall things we did in the project.

  1. Defined appscrits.json files with the appropriate scopes and triggers required for Drive Add-on.
  2. Created a simple card UI to interact with users.
  3. Fetched selected files from the drive with the apps script.
  4. Used Actions and Button Sets to let users interact with our card UI.
  5. Created a simple navigation logic to move between two cards.

Show Some Support

This is Nibesh Khadka from Khadka's Coding Lounge. Find my other blogs on Google Apps Scripts here. I am the owner of Khadka's Coding Lounge. We make websites, mobile applications, google add-ons, and valuable tech blogs. Hire us!, Like, Share and Subscribe to our newsletter.

Also Published here


Written by kcl | As a freelancer, I create addons and scripts to automate google products such as sheets, Gmail, docs with apps script.
Published by HackerNoon on 2022/08/23