Step 9) Click here to: Open your Google Add-Ons settings

This infographic displays the Google Add-Ons setting screen. Follow along one step at a time; enable the developer add-ons, then paste the deployment ID and install the app. It should show up

Once you click install, Google will ask you to confirm that you trust the Developer of the Add-On.

Success!

If you followed all of the steps correctly, the app should install correctly and you are ready to begin using it. Head back to your Gmail account and open up an email from somebody. After a few seconds, an icon should open to the right of your message. This is your Add-On sidebar!

Click the icon shown above to open the demo app. You can also click “+” to explore other published Add-Ons!

PART TWO — Understanding the Code

Now that we’ve got the application installed, we can start looking at the application itself. Let’s return to the appscript.json file:

The key property here is onTriggerfunction. Its value is set to the function that starts our whole app. You can call it anything, so I named it startApp. Open the code.gs file to locate the startApp function. If you want to view the Logger’s log files at any time, simple select View > Logs:

Throughout this tutorial, I’ve made my comments directly in the code. Please follow along to understand the logic behind each function:

function startApp () { // First, we establish the organizationID. There are two options.

// If organizationID already exists as a property in the state's storage, then it gets that value.

// Otherwise, we get the first organization object in the jsonData array.



var organizationID = userProperties.getProperty('organizationID') || organizationSample[0].id;



// Now that the organizationID var is established, we use setProperty to store that ID in our state.



userProperties.setProperty('organizationID', organizationID);



// Next we gather the array of members who share the organizationID of our current selected org.



var members = memberSample.filter(function(member){

return member.organization_id === organizationID

});



// logs are used throughout these docs for testing purposes.



Logger.log("first member on startApp: " + members[0].name)



// We get a current Member ID, either from the state or from the first member in our members array.



var memberID = userProperties.getProperty('currentMemberID') || members[0].id;



userProperties.setProperty('currentMemberID', memberID);



Logger.log("memberID on startApp: " + memberID)



// Finally, the scheduleOptions from appscript.json are filtered by organization id to provide our schedule array.



var schedules = scheduleOptions.filter(function(schedule){

return schedule.organization_id === organizationID

});



var scheduleID = userProperties.getProperty('currentScheduledID') || schedules[0].id;



userProperties.setProperty('currentScheduleID', scheduleID);



Logger.log("schedules[0].name on startApp: " + schedules[0].name);

Logger.log("schedules[1].name on startApp: " + schedules[1].name);



// return the function located in buildSettingsCard.gs, which loads the first card based on our stateful data



return buildSettingsCard()



}

This startApp function loads each time the app is booted up. It does not run when we navigate between pages. As you saw, we test for existing state in our userProperties store. If there is none, we set property defaults. At the end, we return a function called buildSettingsCard(), located in the .gs file of the same name.

// Build Settings Card function buildSettingsCard() {



// create a new card

var card = CardService.newCardBuilder();



// Set name and header title on card

card.setName("SettingsCard").setHeader(CardService.newCardHeader().setTitle('Company Registration'));



// newCardSections are a that Widgets can be painted onto

var sectionSettings = CardService.newCardSection();



// get organization object

var organization = getObjectByID(organizationSample, userProperties.getProperty('organizationID'));

Logger.log("Current Organization on buildSettingsCard: " + organization.name); // get scheduleID



var schedules = scheduleOptions.filter(function(schedule){

return schedule.organization_id === organization.id

});



var scheduleID = userProperties.getProperty('currentScheduledID') || schedules[0].id;



userProperties.setProperty('currentScheduleID', scheduleID);



// get the current member associated with current organization

var currentMember = getObjectByID(memberSample, userProperties.getProperty('currentMemberID'));

Logger.log("Current Member Name on buildSettingsCard: " + currentMember.name);



// get currentMembers to be displayed on the member select menu



var currentMembers = memberSample.filter(function(member){

return member.organization_id === userProperties.getProperty('organizationID');

});



Logger.log("name of member 1 associated with current organization name on buildSettingsCard: " + currentMembers[0].name);

Logger.log("name of member 2 associated with current organization name on buildSettingsCard: " + currentMembers[1].name);



// Add widgets to the Settings card section



// Add the organization select menu widget to the card



sectionSettings.addWidget(getOrganizationSelectMenu(organizationSample, organization || organizationSample[0]));



// check that there are members associated with the current organization. If so, display the member select menu widget on the card



if (currentMembers.length > 0) {



sectionSettings.addWidget(getMembersSelectMenu(currentMembers, currentMember || memberSample[0]));

} else {

sectionSettings.addWidget(CardService.newTextParagraph().setText("No Members Available"));

};



// btnSaveSettings is a button located at the bottom of the card

// actSaveSettings a newAction that triggers a function called buildScheduleTypes.

// buildScheduleTypes is located in the corresponding buildScheduleTypes.gs file



var actSaveSettings = CardService.newAction()

.setFunctionName('buildScheduleTypes');

var btnSaveSettings = CardService.newTextButton()

.setText("Save")

.setOnClickAction(actSaveSettings);



// add the button



sectionSettings.addWidget(btnSaveSettings); // Return and build the card



return card.addSection(sectionSettings).build();



}

This is the first time where you see a new card built and widgets added to one of the card’s sections. CardService widgets are a core feature of Google Add-Ons. The final widget on this card is btnSaveSettings, which is configured to run the function buildScheduleTypes, located in the .gs file of the same name:

// Build Schedule Types function buildScheduleTypes(e) {



var currentMemberID = userProperties.getProperty('currentMemberID');



Logger.log("current member id: " + currentMemberID);





// re-establish state variables for local scope var currentSchedules = scheduleOptions.filter(function(schedule){

return schedule.organization_id === userProperties.getProperty('organizationID')

});



Logger.log("first schedule on currentSchedules: " + currentSchedules[0].name);



// create card and give it header title, establish section



var cardScheduleTypes = CardService.newCardBuilder()

.setName("ScheduleCard")

.setHeader(CardService.newCardHeader().setTitle("Schedule Types"));



var sectionSchedules = CardService.newCardSection();



// add widgets to card section for Schedule types



if (currentSchedules.length != 0) {

sectionSchedules.addWidget(getScheduleSelectMenu(currentSchedules));

} else {

sectionSchedules.addWidget(CardService.newTextParagraph().setText("No Schedule Types Available"));

}



// Print-to-Email Button (see createReply.gs)





var action = CardService.newAction().setFunctionName('createReply');



sectionSchedules.addWidget(CardService

.newTextButton()

.setText('Confirm')

.setComposeAction(action, CardService.ComposedEmailType.REPLY_AS_DRAFT));



return cardScheduleTypes.addSection(sectionSchedules).build();



}

Test Driven Development

Each action in the application is connected to a function with a series of data logs. You can test the app during a variety of stages: initial boot-up, selecting from dropdown menus, navigating to a page, using the universal settings link, and hitting the “confirm” button.

Try keeping your Gmail account open in one tab while you look at the Logs in your App Script IDE. This way you can tab back and forward. If you want to see how different methods are behaving and where they are getting called, these Logger.log() tests can be very helpful.

Wrapping it up:

The main purpose of this article has been to introduce you to Google Add-Ons and give you a working demo that showcases a combination of Select Forms with their REPLY_AS_DRAFT feature. If you want to get an even more granular understanding of the app, check out all the files in the application.

You can find more Google Add-On sample apps on Github here. If you enjoyed this article and want to read more by the author, please see the following Flutter Articles:

Thanks again for reading, and please feel free to leave comments below with any thoughts, feedback, suggestions, questions, or favorite Add-On!