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
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.
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.
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.
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.
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.
For contextual triggers, we'll assign the function in our apps script to drive.onItemSelectedTrigger in the manifest file.
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"
}
}
}
}
Now, on the root project folder create two files, cards and main.
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.
Let's create a simple homepage card to be assigned to our non-contextual trigger on the cards file.
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.
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.
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();
}
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),
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.
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.
// 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.
// 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);
}
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:
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.
To publish an add-on to the GCP follow these two instructions here.
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
}
Alright, let's recall things we did in the project.
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