# Google Workspace Add-on bouwen [[TOC]] ## Workspace Add-on of Editor Add-on Er zijn twee soorten add-ons in het Google-ecosysteem, en het is belangrijk dat je vooraf het juiste type kiest: - **Editor Add-ons**: draaien in Sheets, Docs, Slides en Forms via het menu **Extensies**. Ze zijn alleen beschikbaar als de gebruiker het betreffende bestand open heeft, en kunnen een eigen zijbalk tonen met `HtmlService`. - **Workspace Add-ons**: verschijnen in de zijbalk van Gmail, Drive, Calendar, Docs, Sheets, Slides en Meet. Ze gebruiken het Cards-framework en werken over meerdere apps tegelijk. Dit artikel behandelt Workspace Add-ons op basis van het moderne Cards-framework. Dat is de aanbevolen aanpak als je dezelfde add-on in meerdere Google-apps wilt aanbieden. ## Manifest configureren Het `appsscript.json` manifest definieert de triggers, het runtime en de OAuth-scopes. Schakel in de Apps Script-editor eerst **Project Settings** en de optie **Show appsscript.json manifest file in editor** in. ```json { "timeZone": "Europe/Amsterdam", "dependencies": {}, "exceptionLogging": "STACKDRIVER", "runtimeVersion": "V8", "addOns": { "common": { "name": "Mijn Add-on", "logoUrl": "https://example.com/icon.png", "homepageTrigger": { "runFunction": "buildHomepageCard" } }, "gmail": { "contextualTriggers": [{ "unconditional": {}, "onTriggerFunction": "buildGmailContextCard" }] } }, "oauthScopes": [ "https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/script.locale" ] } ``` De `homepageTrigger` onder `common` bouwt de niet-contextuele beginpagina van je add-on, die in elke ondersteunde host verschijnt. De `contextualTriggers` onder `gmail` reageren op een geopend bericht. Een contextuele Gmail-trigger ondersteunt op dit moment alleen het criterium `unconditional`, wat betekent dat hij voor elk geopend bericht afgaat. :::warn title="Vraag alleen de scopes die je nodig hebt" Beperk de OAuth-scopes tot het minimum dat je add-on echt gebruikt. Brede scopes zoals `gmail.modify` of volledige Drive-toegang vertragen de Marketplace-review en schrikken gebruikers af bij het goedkeuringsscherm. Begin met `readonly`-varianten en breid alleen uit wanneer een functie er echt om vraagt. ::: ## Homepage card bouwen De homepage-functie geeft een `Card`-object terug dat in de zijbalk verschijnt: ```javascript function buildHomepageCard() { const card = CardService.newCardBuilder(); card.setName('homepage'); const header = CardService.newCardHeader() .setTitle('Mijn Workspace Add-on') .setSubtitle('Versie 1.0') .setImageUrl('https://example.com/icon.png'); card.setHeader(header); const sectie = CardService.newCardSection() .setHeader('Snelle acties'); const knop1 = CardService.newTextButton() .setText('Rapport genereren') .setOnClickAction( CardService.newAction().setFunctionName('genereerRapport') ); sectie.addWidget(knop1); card.addSection(sectie); return card.build(); } ``` ## Contextuele Gmail-card Wanneer een gebruiker een e-mail opent, vuurt de contextuele trigger. Het event-object bevat het bericht-ID: ```javascript function buildGmailContextCard(e) { const berichtId = e.gmail.messageId; const bericht = GmailApp.getMessageById(berichtId); const card = CardService.newCardBuilder(); const sectie = CardService.newCardSection() .setHeader('E-mailinformatie'); sectie.addWidget( CardService.newKeyValue() .setTopLabel('Afzender') .setContent(bericht.getFrom()) ); sectie.addWidget( CardService.newKeyValue() .setTopLabel('Datum') .setContent(bericht.getDate().toLocaleDateString('nl-NL')) ); const opslaanKnop = CardService.newTextButton() .setText('Opslaan in Sheets') .setOnClickAction( CardService.newAction() .setFunctionName('slaEmailDetailsOp') .setParameters({berichtId}) ); sectie.addWidget(CardService.newButtonSet().addButton(opslaanKnop)); card.addSection(sectie); return card.build(); } ``` :::info title="Bericht ophalen vanuit de trigger" Contextuele triggers in Gmail ontvangen het bericht-ID in `e.gmail.messageId`. Haal het volledige bericht op met `GmailApp.getMessageById(id)`. Let op: hiervoor heb je de scope `gmail.addons.current.message.readonly` of `gmail.readonly` nodig, anders krijg je een autorisatiefout. ::: ## Action-functies implementeren Een action-functie wordt aangeroepen wanneer de gebruiker op een knop klikt. Ze geeft een `ActionResponse` terug, bijvoorbeeld om een notificatie te tonen: ```javascript function slaEmailDetailsOp(e) { const berichtId = e.parameters.berichtId; const bericht = GmailApp.getMessageById(berichtId); const blad = SpreadsheetApp.openById( PropertiesService.getScriptProperties().getProperty('SHEET_ID') ).getActiveSheet(); blad.appendRow([ new Date(), bericht.getFrom(), bericht.getSubject(), bericht.getDate(), ]); return CardService.newActionResponseBuilder() .setNotification( CardService.newNotification().setText('E-mail opgeslagen in Sheets') ) .build(); } ``` :::tip title="Bewaar configuratie in script properties" Hardcode geen sheet-ID's, API-sleutels of webhook-URL's in je code. Gebruik `PropertiesService.getScriptProperties()` voor instellingen die per deployment verschillen, en `getUserProperties()` voor gebruikersspecifieke voorkeuren. Zo kun je dezelfde code veilig delen en deployen zonder gevoelige waarden in de broncode. ::: ## Formulier-invoer in add-ons Cards ondersteunen invoervelden en keuzelijsten. De ingevulde waarden komen binnen via `e.formInput`: ```javascript function buildFormCard() { const sectie = CardService.newCardSection(); const naamInvoer = CardService.newTextInput() .setFieldName('naam') .setTitle('Naam') .setHint('Volledige naam'); const categorieDropdown = CardService.newSelectionInput() .setType(CardService.SelectionInputType.DROPDOWN) .setFieldName('categorie') .setTitle('Categorie') .addItem('Verkoop', 'verkoop', false) .addItem('Support', 'support', true) .addItem('Administratie', 'admin', false); const verstuurKnop = CardService.newTextButton() .setText('Versturen') .setOnClickAction( CardService.newAction().setFunctionName('verwerkFormulier') ); sectie.addWidget(naamInvoer) .addWidget(categorieDropdown) .addWidget(CardService.newButtonSet().addButton(verstuurKnop)); return CardService.newCardBuilder() .addSection(sectie) .build(); } function verwerkFormulier(e) { const naam = e.formInput.naam; const categorie = e.formInput.categorie; Logger.log(`Formulier: ${naam}, ${categorie}`); return CardService.newActionResponseBuilder() .setNotification(CardService.newNotification().setText(`Bedankt, ${naam}!`)) .build(); } ``` ## Testen en deployen Tijdens de ontwikkeling test je je add-on met een test-deployment, zodat je niet bij elke wijziging opnieuw hoeft te publiceren. :::howto title="Add-on testen en intern uitrollen" 1. Open in de Apps Script-editor **Deploy** en kies **Test deployments**. 2. Klik op **Install** om de testversie in je eigen account te activeren. 3. Open de bijbehorende host, bijvoorbeeld Gmail, en controleer de zijbalk. 4. Pas je code aan en herlaad de host: de testversie gebruikt automatisch je laatste opgeslagen code. 5. Tevreden? Kies **Deploy** en **New deployment** met het type **Add-on**. 6. Deel de add-on intern via het **Script ID** of via de Google Workspace Marketplace SDK voor je domein. ::: Voor publieke verspreiding heb je een Google Cloud-project nodig met een geconfigureerd OAuth-consentscherm, een ingevulde **Google Workspace Marketplace SDK** en een review door Google. Houd er rekening mee dat de review enkele dagen tot weken kan duren, vooral bij gevoelige of restricted scopes. :::faq ### Hoe test ik een add-on voordat ik hem publiceer? Gebruik **Test deployments** in de Apps Script-editor. Daarmee installeer je een testversie in je eigen account die altijd je laatst opgeslagen code draait, zodat je wijzigingen direct in de echte host ziet zonder te publiceren. ### Kan een add-on bij de Drive-bestanden van de gebruiker? Ja, mits je de juiste OAuth-scopes in het manifest opneemt, bijvoorbeeld `drive.file` voor alleen bestanden die de gebruiker opent of `drive.readonly` voor leestoegang. De gebruiker keurt deze scopes goed bij het eerste gebruik. ### Hoe voeg ik mijn add-on toe aan de Workspace Marketplace? Je hebt een Google Cloud-project nodig, een geconfigureerd OAuth-consentscherm en de Google Workspace Marketplace SDK. Daarin vul je de listing, screenshots en scopes in en dien je de add-on ter review in bij Google. ### Wat zijn de beperkingen van de Cards-UI? De Cards-UI ondersteunt een vaste set widgets, zoals tekst, knoppen, afbeeldingen, keyvalue-rijen en formuliervelden. Voor vrij vormgegeven of interactieve HTML gebruik je een Editor Add-on met `HtmlService` in plaats van het Cards-framework. ### Werkt dezelfde add-on in meerdere apps tegelijk? Ja. Je definieert gedeelde logica onder `addOns.common` en voegt per host, zoals `gmail`, `drive` of `calendar`, specifieke triggers toe. Eén deployment levert zo één add-on die in al die apps verschijnt. ### Hoe sla ik gevoelige instellingen veilig op? Gebruik `PropertiesService`. Met `getScriptProperties()` bewaar je deployment-brede waarden zoals een sheet-ID en met `getUserProperties()` gebruikersspecifieke voorkeuren. Zet nooit sleutels of ID's rechtstreeks in je broncode. ::: Workspace Add-ons integreren je eigen functionaliteit naadloos in de dagelijkse Google-tools van je team. Met het Cards-framework bouw je consistente UI die in Gmail, Drive, Docs, Sheets en Calendar werkt, terwijl je met scopes, script properties en test-deployments grip houdt op veiligheid en uitrol.